Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
55e47be
add clean task
elcritch Sep 11, 2025
072f949
add composite key test
elcritch Sep 11, 2025
520ec82
add postgres setup scripts
elcritch Sep 11, 2025
eff70fe
add postgres setup scripts
elcritch Sep 11, 2025
131c079
fix postgres on macosx
elcritch Sep 11, 2025
2d99159
fix postgres
elcritch Sep 11, 2025
d5707d0
fix postgres
elcritch Sep 11, 2025
62669fb
fix postgres test, document using db_connector
elcritch Sep 11, 2025
764035e
fix postgres test, document using db_connector
elcritch Sep 11, 2025
41beb00
run pg tests
elcritch Sep 11, 2025
5f69404
run pg tests
elcritch Sep 11, 2025
1db1448
run pg tests
elcritch Sep 11, 2025
446b1b6
run pg tests
elcritch Sep 11, 2025
30bf3e4
run pg tests
elcritch Sep 11, 2025
55b8724
run pg tests
elcritch Sep 11, 2025
3bb38d8
use pegs
elcritch Sep 11, 2025
6587455
use pegs
elcritch Sep 11, 2025
6973415
Use parsesql for table name detection
elcritch Sep 13, 2025
db15fc4
Merge pull request #6 from elcritch/codex/switch-db_utils-to-uses-par…
elcritch Sep 13, 2025
9beb13d
updates
elcritch Sep 14, 2025
32e6594
updates
elcritch Sep 14, 2025
547d731
updates
elcritch Sep 14, 2025
236f721
updates
elcritch Sep 14, 2025
2f35e8c
fix path handling to allow direct sql from strings to be used
elcritch Sep 14, 2025
9d4c0d2
fix impoprts
elcritch Sep 14, 2025
4f4dbfd
fix impoprts
elcritch Sep 14, 2025
d100435
add test
elcritch Sep 14, 2025
3604685
add test
elcritch Sep 14, 2025
49349fa
add test
elcritch Sep 14, 2025
6d3d4dd
add test
elcritch Sep 14, 2025
f95b7c7
add test
elcritch Sep 14, 2025
eef43b5
Merge branch 'master' into add-composite-pk
Araq Sep 21, 2025
3a8306c
Update tests/model_sqlite.sql
elcritch Sep 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
run: nimble buildexamples
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ examples/tweeter/src/tweeterdeps/
deps/
nim.cfg
.nimcache
tests/tdb_utils
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 23 additions & 9 deletions config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,42 @@ 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()
selfExec "c examples/chat/server"
selfExec "js examples/chat/frontend"
selfExec "c examples/forum/forum"
selfExec "c examples/forum/forumproto"
selfExec "c examples/tweeter/src/tweeter"
selfExec "c examples/tweeter/src/tweeter"
6 changes: 2 additions & 4 deletions ormin.nimble
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
54 changes: 40 additions & 14 deletions ormin/db_utils.nim
Original file line number Diff line number Diff line change
@@ -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))
db.exec(sql("drop table if exists " & n))
4 changes: 2 additions & 2 deletions ormin/ormin_postgre.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 14 additions & 1 deletion tools/parsesql_tmp.nim → ormin/parsesql_tmp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions ormin/queries.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions tests/db_utils_case_quoted.sql
Original file line number Diff line number Diff line change
@@ -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
);
12 changes: 6 additions & 6 deletions tests/forum_model_postgres.sql
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -57,4 +57,4 @@ create table if not exists error(
);

create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
create index PostByAuthorIdx on post(thread, author);
Loading