diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec23b13..46ee496 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,26 @@ name: CI -on: [push] +on: [push, pull_request] jobs: build: runs-on: ubuntu-latest + + services: + postgres: + image: postgres:14 + env: + POSTGRES_PASSWORD: test + POSTGRES_USER: test + POSTGRES_DB: test_ormin + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + strategy: matrix: nim: @@ -33,29 +49,20 @@ jobs: with: nim-version: ${{ matrix.nim }} - - name: Setup postgresql - uses: harmon758/postgresql-action@v1 - with: - postgresql version: '14' - postgresql user: 'test' - postgresql password: 'test' - postgresql db: 'test' - - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libpcre3-dev - - name: Install dependencies run: | nimble --useSystemNim install -d nimble --useSystemNim install karax -y nimble --useSystemNim install jester -y + nimble --useSystemNim install websocket -y - - name: Run test + - name: Run test sqlite run: nimble --useSystemNim test + + - name: Run test postgres + run: nimble --useSystemNim test_postgres env: PGPASSWORD: test - name: Build examples - run: nimble buildexamples \ No newline at end of file + run: nimble buildexamples diff --git a/.gitignore b/.gitignore index d5f9ada..28353e0 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ examples/tweeter/src/tweeterdeps/ deps/ nim.cfg .nimcache +tests/tdb_utils diff --git a/README.md b/README.md index cac5a52..01cd9e0 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,14 @@ for row in db.postsIter(userId): Both forms accept parameters matching the `?`/`%` placeholders and produce the same return types as an inline `query` block. +## Running Arbitrary SQL + +The standard `db_connector` APIs can be imported and used. For example: + +```nim +discard db.getValue(sql"select setval('antibot_id_seq', 10, false)") +``` + ## Additional Facilities - **Protocol DSL** – The `protocol` macro lets you describe paired server/client handlers that communicate via JSON messages. Sections use keywords like `recv`, `broadcast` and `send`, and every server block must be mirrored by a client block. The chat example demonstrates this code generation. diff --git a/config.nims b/config.nims index 189a8fb..08eca33 100644 --- a/config.nims +++ b/config.nims @@ -3,23 +3,37 @@ switch("nimcache", ".nimcache") task buildimporter, "Build ormin_importer": exec "nim c -o:./tools/ormin_importer tools/ormin_importer" -task test, "Run all test suite": - buildimporterTask() +task clean, "Clean generated files": rmFile("tests/forum_model_sqlite.nim") rmFile("tests/model_sqlite.nim") + rmFile("tests/forum_model_postgres.nim") + rmFile("tests/model_postgre.nim") + +task test, "Run all test suite": + buildimporterTask() + cleanTask() + exec "nim c -f -r tests/tfeature" exec "nim c -f -r tests/tcommon" exec "nim c -f -r tests/tsqlite" + exec "nim c -f -r tests/tdb_utils" + +task setup_postgres, "Ensure local Postgres has test DB/user": + # Use a simple script to avoid Nim/psql quoting pitfalls + exec "bash -lc 'bash tools/setup_postgres.sh'" task test_postgres, "Run PostgreSQL test suite": - # Skip PostgreSQL tests as they require a running PostgreSQL server - rmFile("tests/forum_model_postgres.nim") - rmFile("tests/model_postgre.nim") + cleanTask() + buildimporterTask() + # setup_postgresTask() + # Pre-generate Postgres models to avoid include timing issues + exec "./tools/ormin_importer tests/forum_model_postgres.sql" + exec "./tools/ormin_importer tests/model_postgre.sql" - exec "nim c -r -d:postgre tests/tfeature" - exec "nim c -r -d:postgre tests/tcommon" - exec "nim c -r tests/tpostgre" + exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature" + exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon" + exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre" task buildexamples, "Build examples: chat and forum": buildimporterTask() @@ -27,4 +41,4 @@ task buildexamples, "Build examples: chat and forum": selfExec "js examples/chat/frontend" selfExec "c examples/forum/forum" selfExec "c examples/forum/forumproto" - selfExec "c examples/tweeter/src/tweeter" \ No newline at end of file + selfExec "c examples/tweeter/src/tweeter" diff --git a/ormin.nimble b/ormin.nimble index 23e4d37..9e582f6 100644 --- a/ormin.nimble +++ b/ormin.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.0" +version = "0.4.0" author = "Araq" description = "Prepared SQL statement generator. A lightweight ORM." license = "MIT" @@ -12,11 +12,9 @@ installExt = @["nim"] requires "nim >= 2.0.0" requires "db_connector >= 0.1.0" -feature "websocket": +feature "examples": requires "websocket >= 0.2.2" -feature "karax": requires "karax" -feature "jester": requires "jester" include "config.nims" diff --git a/ormin/db_utils.nim b/ormin/db_utils.nim index f424657..54ec3fb 100644 --- a/ormin/db_utils.nim +++ b/ormin/db_utils.nim @@ -1,38 +1,64 @@ -import db_connector/db_common, strutils, strformat, re -from db_connector/db_postgres import nil -from db_connector/db_sqlite import nil +import std/paths +import db_connector/db_common, strutils, strformat +import db_connector/db_postgres as db_postgres +import db_connector/db_sqlite as db_sqlite + +import parsesql_tmp # import std/parsesql + +export paths type DbConn = db_postgres.DbConn | db_sqlite.DbConn -iterator tablePairs(sqlFile: string): tuple[name, model: string] = - let f = readFile(sqlFile) - for m in f.split(';'): - if m.strip() != "" and - m =~ re"\n*create\s+table(\s+if\s+not\s+exists)?\s+(\w+)": - yield (matches[1], m) +iterator tablePairs*(sql: string): tuple[name, model: string] = + # Parse the entire SQL file and iterate statements via the SQL parser + var ast: SqlNode + try: + ast = parseSql(sql) + except SqlParseError as e: + echo "SQL Parse Error:\n", sql + raise e + + if ast.len > 0: + # ast is a statement list; iterate each statement node + for i in 0 ..< ast.len: + let node = ast[i] + if node.kind in {nkCreateTable, nkCreateTableIfNotExists}: + let tableName = node[0].strVal.toLowerAscii() + yield (tableName, $node) + else: + # Fallback: ast might be a single statement (not a list) + let node = ast + if node.kind in {nkCreateTable, nkCreateTableIfNotExists}: + let tableName = node[0].strVal.toLowerAscii() + yield (tableName, $node) + +iterator tablePairs*(sql: Path): tuple[name, model: string] = + let f = readFile($sql) + for n, m in tablePairs(f): + yield (n, m) -proc createTable*(db: DbConn; sqlFile: string) = +proc createTable*(db: DbConn; sqlFile: Path) = for _, m in tablePairs(sqlFile): db.exec(sql(m)) -proc createTable*(db: DbConn; sqlFile, name: string) = +proc createTable*(db: DbConn; sqlFile: Path, name: string) = for n, m in tablePairs(sqlFile): if n == name: db.exec(sql(m)) return raiseAssert &"table: {name} not found in: {sqlFile}" -proc dropTable*(db: DbConn; sqlFile: string) = +proc dropTable*(db: DbConn; sqlFile: Path) = for n, _ in tablePairs(sqlFile): when defined(postgre): db.exec(sql("drop table if exists " & n & " cascade")) else: db.exec(sql("drop table if exists " & n)) -proc dropTable*(db: DbConn; sqlFile, name: string) = +proc dropTable*(db: DbConn; sqlFile: Path, name: string) = for n, _ in tablePairs(sqlFile): if n == name: when defined(postgre): db.exec(sql("drop table if exists " & n & " cascade")) else: - db.exec(sql("drop table if exists " & n)) \ No newline at end of file + db.exec(sql("drop table if exists " & n)) diff --git a/ormin/ormin_postgre.nim b/ormin/ormin_postgre.nim index 7f5248c..fce1ab9 100644 --- a/ormin/ormin_postgre.nim +++ b/ormin/ormin_postgre.nim @@ -212,14 +212,14 @@ template startQuery*(db: DbConn; s: PStmt) = nil, nil, nil, 0) if pqResultStatus(queryResult) == PGRES_COMMAND_OK: discard # insert does not returns data in pg - elif pqResultStatus(queryResult) != PGRES_TUPLES_OK: dbError(db) + elif pqResultStatus(queryResult) != PGRES_TUPLES_OK: ormin_postgre.dbError(db) var queryI {.inject.} = cint(-1) var queryLen {.inject.} = pqntuples(queryResult) template stopQuery*(db: DbConn; s: PStmt) = pqclear(queryResult) -template stepQuery*(db: DbConn; s: PStmt; returnsData: int): bool = +template stepQuery*(db: DbConn; s: PStmt; returnsData: bool): bool = inc queryI queryI < queryLen diff --git a/tools/parsesql_tmp.nim b/ormin/parsesql_tmp.nim similarity index 99% rename from tools/parsesql_tmp.nim rename to ormin/parsesql_tmp.nim index 21677d4..d3ddbc8 100644 --- a/tools/parsesql_tmp.nim +++ b/ormin/parsesql_tmp.nim @@ -1536,7 +1536,20 @@ proc ra(n: SqlNode, s: var SqlWriter) = rs(n, s) of nkForeignKey: s.addKeyw("foreign key") - rs(n, s) + # Render only the column identifiers inside parentheses, + # then append the REFERENCES clause (and options) after. + var i = 0 + s.add('(') + while i < n.len and n.sons[i].kind != nkReferences: + if i > 0: s.add(", ") + ra(n.sons[i], s) + inc i + s.add(')') + # If a REFERENCES node exists, render it after the column list + while i < n.len: + s.add(' ') + ra(n.sons[i], s) + inc i of nkNotNull: s.addKeyw("not null") of nkNull: diff --git a/ormin/queries.nim b/ormin/queries.nim index 12c7c7b..dc231f9 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -143,11 +143,12 @@ proc checkBool(a: DbType; n: NimNode) = macros.error "expected type 'bool', but got: " & $a, n proc checkInt(a: DbType; n: NimNode) = - if a.kind != dbInt: + if a.kind notin {dbInt, dbSerial}: macros.error "expected type 'int', but got: " & $a, n proc checkCompatible(a, b: DbType; n: NimNode) = - if a.kind != b.kind: + # Treat serial and int as compatible + if not (a.kind == b.kind or (a.kind == dbSerial and b.kind == dbInt) or (a.kind == dbInt and b.kind == dbSerial)): macros.error "incompatible types: " & $a & " and " & $b, n proc checkCompatibleSet(a, b: DbType; n: NimNode) = @@ -454,6 +455,8 @@ proc generateRoutine(name: NimNode, q: QueryBuilder; let prepare = newVarStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit(sql))) let body = newStmtList() + # Ensure the prepared statement is created before binding/starting the query + body.add prepare var finalParams = newNimNode(nnkFormalParams) if q.retTypeIsJson: @@ -869,14 +872,11 @@ proc queryImpl(q: QueryBuilder; body: NimNode; attempt, produceJson: bool): NimN if b.kind == nnkCommand: queryh(b, q) else: macros.error "illformed query", b let sql = queryAsString(q, body) - let prepStmt = genSym(nskVar) + let prepStmt = genSym(nskLet) let res = genSym(nskVar) - let prepStmtCall = newCall(bindSym"prepareStmt", ident"db", newLit sql) result = newTree( if q.retType.len > 0: nnkStmtListExpr else: nnkStmtList, - # really hack-ish - newGlobalVar(prepStmt, newCall(bindSym"typeof", prepStmtCall), newEmptyNode()), - getAst(once(newAssignment(prepStmt, prepStmtCall))) + newLetStmt(prepStmt, newCall(bindSym"prepareStmt", ident"db", newLit sql)) ) let rtyp = if q.retType.len > 1 or q.retType.len == 0: q.retType diff --git a/tests/db_utils_case_quoted.sql b/tests/db_utils_case_quoted.sql new file mode 100644 index 0000000..cc25649 --- /dev/null +++ b/tests/db_utils_case_quoted.sql @@ -0,0 +1,18 @@ +-- lower case, upper case, and quoted table names + create table lower_table ( + id integer primary key + ); + + CREATE TABLE UPPER_TABLE ( + id integer primary key + ); + + -- std sql quoted table name + create table "Quoted Table" ( + id integer primary key + ); + + -- sqlite quoted table name + create table `Quoted Table2` ( + id integer primary key + ); diff --git a/tests/forum_model_postgres.sql b/tests/forum_model_postgres.sql index 8ed5457..8ce68b0 100644 --- a/tests/forum_model_postgres.sql +++ b/tests/forum_model_postgres.sql @@ -1,5 +1,5 @@ create table if not exists thread( - id integer primary key, + id serial primary key, name varchar(100) not null, views integer not null, modified timestamp not null default CURRENT_TIMESTAMP @@ -8,7 +8,7 @@ create table if not exists thread( create unique index if not exists ThreadNameIx on thread (name); create table if not exists person( - id integer primary key, + id serial primary key, name varchar(20) not null, password varchar(32) not null, email varchar(30) not null, @@ -22,7 +22,7 @@ create table if not exists person( create unique index if not exists UserNameIx on person (name); create table if not exists post( - id integer primary key, + id serial primary key, author integer not null, ip varchar(20) not null, header varchar(100) not null, @@ -35,7 +35,7 @@ create table if not exists post( ); create table if not exists session( - id integer primary key, + id serial primary key, ip varchar(20) not null, password varchar(32) not null, userid integer not null, @@ -44,7 +44,7 @@ create table if not exists session( ); create table if not exists antibot( - id integer primary key, + id serial primary key, ip varchar(20) not null, answer varchar(30) not null, created timestamp not null default CURRENT_TIMESTAMP @@ -57,4 +57,4 @@ create table if not exists error( ); create index PersonStatusIdx on person(status); -create index PostByAuthorIdx on post(thread, author); \ No newline at end of file +create index PostByAuthorIdx on post(thread, author); diff --git a/tests/model_postgre.sql b/tests/model_postgre.sql index 52aabe5..ac6c4e9 100644 --- a/tests/model_postgre.sql +++ b/tests/model_postgre.sql @@ -12,7 +12,7 @@ create table if not exists tb_float( ); create table if not exists tb_string( - typstring varchar not null + typstring text not null ); create table if not exists tb_timestamp( @@ -23,4 +23,18 @@ create table if not exists tb_timestamp( create table if not exists tb_json( typjson json not null -); \ No newline at end of file +); + +create table if not exists tb_composite_pk( + pk1 integer not null, + pk2 integer not null, + message text not null, + primary key (pk1, pk2) +); + +create table if not exists tb_composite_fk( + id integer not null, + fk1 integer not null, + fk2 integer not null, + foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2) +); diff --git a/tests/model_sqlite.sql b/tests/model_sqlite.sql index aa50605..8b4ecee 100644 --- a/tests/model_sqlite.sql +++ b/tests/model_sqlite.sql @@ -24,6 +24,19 @@ create table if not exists tb_json( typjson json not null ); +create table if not exists tb_composite_pk( + pk1 integer not null, + pk2 integer not null, + message text not null, + primary key (pk1, pk2) +); + +create table if not exists tb_composite_fk( + id integer not null, + fk1 integer not null, + fk2 integer not null, + foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2) +); create table if not exists tb_blob( id integer primary key, typblob blob not null diff --git a/tests/tcommon.nim b/tests/tcommon.nim index 6df6db6..326f070 100644 --- a/tests/tcommon.nim +++ b/tests/tcommon.nim @@ -3,12 +3,14 @@ import ormin import ormin/db_utils when defined(postgre): - from db_postgres import exec, getValue + when defined(macosx): + {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".} + import db_connector/db_postgres as db_postgres const backend = DbBackend.postgre importModel(backend, "model_postgre") const sqlFileName = "model_postgre.sql" - let db {.global.} = open("localhost", "test", "test", "test") + let db {.global.} = db_postgres.open("localhost", "test", "test", "test_ormin") else: from db_connector/db_sqlite import exec, getValue @@ -20,7 +22,7 @@ else: let testDir = currentSourcePath.parentDir() - sqlFile = testDir / sqlFileName + sqlFile = Path(testDir / sqlFileName) let @@ -338,3 +340,55 @@ suite "json": select tb_json(typjson) produce json check res == %*js.mapIt(%*{"typjson": it}) + + +let cps = [ + (1, 1, "one-one"), + (1, 2, "one-two"), + (2, 1, "two-one") +] + +suite "composite_pk_insert": + setup: + db.dropTable(sqlFile, "tb_composite_pk") + db.createTable(sqlFile, "tb_composite_pk") + + test "insert": + for r in cps: + query: + insert tb_composite_pk(pk1 = ?r[0], pk2 = ?r[1], message = ?r[2]) + check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len + + test "json": + for r in cps: + let v = %*{"pk1": r[0], "pk2": r[1], "message": r[2]} + query: + insert tb_composite_pk(pk1 = %v["pk1"], pk2 = %v["pk2"], message = %v["message"]) + check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len + + +suite "composite_pk": + db.dropTable(sqlFile, "tb_composite_pk") + db.createTable(sqlFile, "tb_composite_pk") + + let insertSql = sql"insert into tb_composite_pk(pk1, pk2, message) values (?, ?, ?)" + for r in cps: + db.exec(insertSql, r[0], r[1], r[2]) + doAssert db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len + + test "query": + let res = query: + select tb_composite_pk(pk1, pk2, message) + check res == cps + + test "where": + let res = query: + select tb_composite_pk(pk1, pk2, message) + where pk1 == ?cps[0][0] + check res == cps.filterIt(it[0] == cps[0][0]) + + test "json": + let res = query: + select tb_composite_pk(pk1, pk2, message) + produce json + check res == %*cps.mapIt(%*{"pk1": it[0], "pk2": it[1], "message": it[2]}) diff --git a/tests/tdb_utils.nim b/tests/tdb_utils.nim new file mode 100644 index 0000000..8e1b54c --- /dev/null +++ b/tests/tdb_utils.nim @@ -0,0 +1,57 @@ +import unittest, os, sequtils +import db_connector/db_common +from db_connector/db_sqlite import open, exec, getValue +import ormin/db_utils + +let + db {.global.} = open(":memory:", "", "", "") + testDir = currentSourcePath.parentDir() + sqlFile = Path(testDir / "db_utils_case_quoted.sql") + +let sqlContent = """ +-- lower case, upper case, and quoted table names + create table lower_table ( + id integer primary key + ); + + CREATE TABLE UPPER_TABLE ( + id integer primary key + ); + + -- std sql quoted table name + create table "Quoted Table" ( + id integer primary key + ); + + -- sqlite quoted table name + create table `Quoted Table2` ( + id integer primary key + ); +""" + +writeFile($sqlFile, sqlContent) + +suite "db_utils: case and quoted names": + test "check tables names": + let pairs = tablePairs(sqlContent).toSeq() + check pairs.len == 4 + check pairs[0][0] == ("lower_table") + check pairs[1][0] == ("upper_table") + check pairs[2][0] == ("quoted table") + check pairs[3][0] == ("quoted table2") + + check pairs[0][1] == "create table lower_table(id integer primary key );" + check pairs[1][1] == "create table UPPER_TABLE(id integer primary key );" + check pairs[2][1] == "create table \"Quoted Table\"(id integer primary key );" + + test "createTable creates all tables from SQL file": + db.createTable(sqlFile) + let countAll = db.getValue(sql"select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table')") + check countAll == "3" + + test "createTable with specific lowercased name matches quoted": + # Use a new in-memory DB for isolation + let db2 = open(":memory:", "", "", "") + db2.createTable(sqlFile, "quoted table") + let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table'") + check countQuoted == "1" diff --git a/tests/tfeature.nim b/tests/tfeature.nim index 7849d85..d796c3f 100644 --- a/tests/tfeature.nim +++ b/tests/tfeature.nim @@ -8,12 +8,14 @@ when NimVersion < "1.2.0": import ./compat let testDir = currentSourcePath.parentDir() when defined postgre: + when defined(macosx): + {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".} from db_connector/db_postgres import exec, getValue const backend = DbBackend.postgre importModel(backend, "forum_model_postgres") const sqlFileName = "forum_model_postgres.sql" - let db {.global.} = open("localhost", "test", "test", "test") + let db {.global.} = open("localhost", "test", "test", "test_ormin") else: from db_connector/db_sqlite import exec, getValue @@ -23,7 +25,7 @@ else: var memoryPath = testDir & "/" & ":memory:" let db {.global.} = open(memoryPath, "", "", "") -var sqlFilePath = testDir & "/" & sqlFileName +var sqlFilePath = Path(testDir & "/" & sqlFileName) type Person = tuple[id: int, @@ -381,10 +383,11 @@ suite "query": check res.sortedByIt(it) == expectedpost.sortedByIt(it) test "subquery_nest3": - let res = query: + var res = query: select thread(id) where id in (select post(thread) where author in (select person(id) where id in {1, 2})) + res.sort() check res == postdata.filterIt(it.author in [1, 2]) .mapIt(it.thread) .sortedByIt(it) @@ -527,7 +530,7 @@ suite "query": test "insert_return_answer": # test returning non-id parameter - let expectedanswer = "just insert" + let expectedanswer = "just insert another" let answer = query: insert antibot(id = 9, ip = "", answer = ?expectedanswer) returning answer @@ -535,8 +538,11 @@ suite "query": test "insert_return_id_auto": # test returning id column + when defined(postgre): + # fix postgres sequence so next nextval returns 10 + discard db.getValue(sql"select setval('antibot_id_seq', ?, ?)", 10, false) let answer = query: - insert antibot(ip = "", answer = "just another insert") + insert antibot(ip = "", answer = "just auto insert") returning id check answer == 10 diff --git a/tests/tpostgre.nim b/tests/tpostgre.nim index ff7db69..0929252 100644 --- a/tests/tpostgre.nim +++ b/tests/tpostgre.nim @@ -1,16 +1,21 @@ import unittest, json, strutils, macros, times, os, sequtils -import db_connector/postgres +# Postgres connection handled through ormin_postgre backend +from db_connector/db_postgres import exec, getValue import ormin +import ormin/ormin_postgre as ormin_postgre +# import db_connector/db_postgres as db_postgres +import ormin/db_utils + +when defined(macosx): + {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".} -from db_connector/postgres import exec, getValue -import ./utils importModel(DbBackend.postgre, "model_postgre") let - db {.global.} = open("localhost", "test", "test", "test") + db {.global.} = ormin_postgre.open("localhost", "test", "test", "test_ormin") testDir = currentSourcePath.parentDir() - sqlFile = testDir / "model_postgre.sql" + sqlFile = Path(testDir / "model_postgre.sql") suite "Test special database types and functions of postgre": diff --git a/tests/tsqlite.nim b/tests/tsqlite.nim index e044972..e7d9c21 100644 --- a/tests/tsqlite.nim +++ b/tests/tsqlite.nim @@ -8,7 +8,7 @@ importModel(DbBackend.sqlite, "model_sqlite") let db {.global.} = open(":memory:", "", "", "") testDir = currentSourcePath.parentDir() - sqlFile = testDir / "model_sqlite.sql" + sqlFile = Path(testDir / "model_sqlite.sql") suite "Test special database types and functions of sqlite": diff --git a/tools/ormin_importer.nim b/tools/ormin_importer.nim index 38c402c..f348099 100644 --- a/tools/ormin_importer.nim +++ b/tools/ormin_importer.nim @@ -5,7 +5,7 @@ import streams, strutils, os, parseopt, tables import db_connector/db_common -import ./parsesql_tmp +import ../ormin/parsesql_tmp #import compiler / [ast, renderer] @@ -125,17 +125,40 @@ proc collectTables(n: SqlNode; t: var KnownTables) = typ: typ, primaryKey: hasAttribute(it, {nkPrimaryKey}), refs: hasRefs(it)) + # Handle table-level foreign keys, including composite FKs for i in 1../dev/null 2>&1; then + psql -v ON_ERROR_STOP=1 -U postgres -d "$db" -f "$file" "$@" + fi +} + +run_psql_cmd() { + local db="$1"; shift + local cmd="$1"; shift + if ! psql -v ON_ERROR_STOP=1 -d "$db" -c "$cmd" "$@" >/dev/null 2>&1; then + psql -v ON_ERROR_STOP=1 -U postgres -d "$db" -c "$cmd" "$@" + fi +} + +# Create role 'test' if needed +run_psql_file postgres tools/setup_postgres_role.sql + +# Create database 'test' if needed (must be outside DO block) +if ! psql -v ON_ERROR_STOP=1 -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname = 'test_ormin'" | grep -q 1; then + run_psql_cmd postgres "CREATE DATABASE test_ormin OWNER test" +fi + +# Grant privileges on public schema +run_psql_cmd test_ormin "GRANT ALL PRIVILEGES ON SCHEMA public TO test" + +echo "Postgres test DB/user ensured (role 'test', db 'test_ormin')." + diff --git a/tools/setup_postgres.sql b/tools/setup_postgres.sql new file mode 100644 index 0000000..b0dc3c8 --- /dev/null +++ b/tools/setup_postgres.sql @@ -0,0 +1,21 @@ +DO +$$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'test') THEN + CREATE ROLE test LOGIN PASSWORD 'test'; + END IF; +END +$$; + +DO +$$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'test') THEN + CREATE DATABASE test OWNER test; + END IF; +END +$$; + +\connect test +GRANT ALL PRIVILEGES ON SCHEMA public TO test; + diff --git a/tools/setup_postgres_role.sql b/tools/setup_postgres_role.sql new file mode 100644 index 0000000..6a19451 --- /dev/null +++ b/tools/setup_postgres_role.sql @@ -0,0 +1,9 @@ +DO +$$ +BEGIN + IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'test') THEN + CREATE ROLE test LOGIN PASSWORD 'test'; + END IF; +END +$$; +