From d4b970235636a785f29428ddef9adfcd9c8ae6ae Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 16:23:09 -0600 Subject: [PATCH 1/9] /Users/elcritch/.local/share/grabnim/current/bin/nim c -r /Volumes/projects/nims/ormin/tests/tcommon.nim --- ormin/db_types.nim | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 ormin/db_types.nim diff --git a/ormin/db_types.nim b/ormin/db_types.nim new file mode 100644 index 0000000..29cc10f --- /dev/null +++ b/ormin/db_types.nim @@ -0,0 +1,43 @@ +import std/strutils +import db_connector/db_common + +proc dbTypFromName*(name: string): DbTypeKind = + var k = dbUnknown + + case name.toLowerAscii + of "int", "integer", "int8", "smallint", "int16", + "longint", "int32", "int64", "tinyint", "hugeint": k = dbInt + of "uint", "uint8", "uint16", "uint32", "uint64": k = dbUInt + of "serial": k = dbSerial + of "bit": k = dbBit + of "bool", "boolean": k = dbBool + of "blob": k = dbBlob + of "fixedchar": k = dbFixedChar + of "varchar", "text", "string": k = dbVarchar + of "json": k = dbJson + of "xml": k = dbXml + of "decimal": k = dbDecimal + of "float", "double", "longdouble", "real": k = dbFloat + of "date", "day": k = dbDate + of "time": k = dbTime + of "datetime": k = dbDateTime + of "timestamp", "timestamptz": k = dbTimestamp + of "timeinterval": k = dbTimeInterval + of "set": k = dbSet + of "array": k = dbArray + of "composite": k = dbComposite + of "url", "uri": k = dbUrl + of "uuid": k = dbUuid + of "inet", "ip", "tcpip": k = dbInet + of "mac", "macaddress": k = dbMacAddress + of "geometry": k = dbGeometry + of "point": k = dbPoint + of "line": k = dbLine + of "lseg": k = dbLseg + of "box": k = dbBox + of "path": k = dbPath + of "polygon": k = dbPolygon + of "circle": k = dbCircle + else: discard + + return k From 4572b7780f197c9911046ca72db911e9a6565046 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 16:41:24 -0600 Subject: [PATCH 2/9] implement importSql pragma that allows adding custom sql functions to Ormin queries. --- .gitignore | 3 ++ ormin/queries.nim | 75 ++++++++++++++++++++++++++++++++++++++++ tests/tcommon.nim | 30 +++++++++++++++- tools/ormin_importer.nim | 38 +------------------- 4 files changed, 108 insertions(+), 38 deletions(-) diff --git a/.gitignore b/.gitignore index d5f9ada..dd9bb66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +* +!*/ +!*.* *.db *.js nimcache diff --git a/ormin/queries.nim b/ormin/queries.nim index 12c7c7b..932b878 100644 --- a/ormin/queries.nim +++ b/ormin/queries.nim @@ -9,6 +9,8 @@ import macros, strutils import db_connector/db_common from os import parentDir, `/` +import db_types + # SQL dialect specific things: const equals = "=" @@ -37,6 +39,79 @@ var Function(name: "replace", arity: 3, typ: dbVarchar) ] +proc isVarargsType(n: NimNode): bool {.compileTime.} = + ## Checks if the provided type node encodes a varargs parameter. + case n.kind + of nnkBracketExpr: + if n.len > 0: + let head = n[0] + if head.kind in {nnkIdent, nnkSym} and head.strVal == "varargs": + return true + else: + discard + result = false + +proc typeNodeToDbKind(n: NimNode): DbTypeKind {.compileTime.} = + ## Maps the return type of an imported SQL function to DbTypeKind. + if n.kind == nnkEmpty: + return dbUnknown + if isVarargsType(n): + if n.len > 1: + return typeNodeToDbKind(n[1]) + return dbUnknown + let name = + case n.kind + of nnkSym, nnkIdent: + n.strVal + else: + $n + return dbTypFromName(name) + +proc registerImportSqlFunction(name: string; arity: int; typ: DbTypeKind) {.compileTime.} = + ## Adds or updates a Function descriptor for vendor specific SQL routines. + let lname = name.toLowerAscii() + for f in mitems(functions): + if f.name.toLowerAscii() == lname and f.arity == arity: + f.typ = typ + return + let f = Function(name: name, arity: arity, typ: typ) + when defined(debugOrminDsl): + echo "registerImportSqlFunction: ", f + functions.add f + +macro importSql*(n: typed): untyped = + ## Registers a Nim proc as callable SQL function within the query DSL. + if n.kind notin {nnkProcDef, nnkFuncDef}: + macros.error("{.importSql.} can only be applied to proc or func definitions", n) + let params = n[3] + expectKind(params, nnkFormalParams) + var paramCount = 0 + var hasVarargs = false + for i in 1.. 2: + macros.error("varargs parameters cannot be combined with fixed parameters", identDefs) + if identDefs.len - 2 != 1: + macros.error("varargs parameter must declare exactly one identifier", identDefs) + hasVarargs = true + else: + paramCount += identDefs.len - 2 + let arity = if hasVarargs: -1 else: paramCount + let fnName = n[0].strVal + let retKind = typeNodeToDbKind(params[0]) + registerImportSqlFunction(fnName, arity, retKind) + result = newStmtList() + type Env = seq[(int, string)] Params = seq[tuple[ex, typ: NimNode; isJson: bool]] diff --git a/tests/tcommon.nim b/tests/tcommon.nim index 6df6db6..d677f07 100644 --- a/tests/tcommon.nim +++ b/tests/tcommon.nim @@ -22,6 +22,7 @@ let testDir = currentSourcePath.parentDir() sqlFile = testDir / sqlFileName +proc substr(s: string; start, length: int): string {.importSql.} let min = -3 @@ -168,6 +169,11 @@ suite "boolean": produce json check res == %*seqs.mapIt(%*{"typboolean": toBool(it)}) + test "importSql substr wrong type": + ## TODO: should this be allowed? + let res = query: + select tb_boolean(substr(typboolean, 1, 2)) + echo "res: ", res let fs = [-3.14, 2.56, 10.45] @@ -221,9 +227,17 @@ suite "float": select tb_float(abs(typfloat)) check res == fs.mapIt(abs(it)) - let ss = ["one", "Two", "three", "第四", "four'th"] +proc firstRunes(s: string; count: int): string = + result = "" + var i = 0 + for r in s.runes: + if i >= count: + break + result.add(r.toUTF8) + inc i + suite "string_insert": setup: db.dropTable(sqlFile, "tb_string") @@ -300,7 +314,21 @@ suite "string": let res = query: select tb_string(replace(typstring, "e", "o")) check res == ss.mapIt(it.replace("e", "o")) + + test "importSql substr length 0": + let res = query: + select tb_string(substr(typstring, 1, 0)) + let expected = ss.mapIt(firstRunes(it, 0)) + check res == expected + test "importSql substr length no arg types checked": + # TODO: fixme! + # the `functions` array only contains the types of the return + # but sqlite doesn't really care... + let res = query: + select tb_string(substr(typstring, "1", 2)) + let expected = ss.mapIt(firstRunes(it, 2)) + check res == expected let js = [ %*{"name": "tom", "age": 30}, diff --git a/tools/ormin_importer.nim b/tools/ormin_importer.nim index 38c402c..92a31fb 100644 --- a/tools/ormin_importer.nim +++ b/tools/ormin_importer.nim @@ -70,43 +70,7 @@ proc getType(n: SqlNode): DbType = assert it[i].kind == nkStringLit result.validValues.add it[i].strVal elif it.kind in {nkIdent, nkStringLit}: - var k = dbUnknown - case it.strVal.toLowerAscii - of "int", "integer", "int8", "smallint", "int16", - "longint", "int32", "int64", "tinyint", "hugeint": k = dbInt - of "uint", "uint8", "uint16", "uint32", "uint64": k = dbUInt - of "serial": k = dbSerial - of "bit": k = dbBit - of "bool", "boolean": k = dbBool - of "blob": k = dbBlob - of "fixedchar": k = dbFixedChar - of "varchar", "text", "string": k = dbVarchar - of "json": k = dbJson - of "xml": k = dbXml - of "decimal": k = dbDecimal - of "float", "double", "longdouble", "real": k = dbFloat - of "date", "day": k = dbDate - of "time": k = dbTime - of "datetime": k = dbDateTime - of "timestamp", "timestamptz": k = dbTimestamp - of "timeinterval": k = dbTimeInterval - of "set": k = dbSet - of "array": k = dbArray - of "composite": k = dbComposite - of "url", "uri": k = dbUrl - of "uuid": k = dbUuid - of "inet", "ip", "tcpip": k = dbInet - of "mac", "macaddress": k = dbMacAddress - of "geometry": k = dbGeometry - of "point": k = dbPoint - of "line": k = dbLine - of "lseg": k = dbLseg - of "box": k = dbBox - of "path": k = dbPath - of "polygon": k = dbPolygon - of "circle": k = dbCircle - else: discard - result.kind = k + result.kind = dbTypFromName(it.strVal) result.name = it.strVal proc collectTables(n: SqlNode; t: var KnownTables) = From f8c86fe508161326acd08ce6e475a3fe64e69f82 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 16:47:22 -0600 Subject: [PATCH 3/9] cleanup --- tests/tcommon.nim | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/tcommon.nim b/tests/tcommon.nim index d677f07..9c6c387 100644 --- a/tests/tcommon.nim +++ b/tests/tcommon.nim @@ -229,15 +229,6 @@ suite "float": let ss = ["one", "Two", "three", "第四", "four'th"] -proc firstRunes(s: string; count: int): string = - result = "" - var i = 0 - for r in s.runes: - if i >= count: - break - result.add(r.toUTF8) - inc i - suite "string_insert": setup: db.dropTable(sqlFile, "tb_string") @@ -318,7 +309,7 @@ suite "string": test "importSql substr length 0": let res = query: select tb_string(substr(typstring, 1, 0)) - let expected = ss.mapIt(firstRunes(it, 0)) + let expected = ss.mapIt($(toRunes(it)[0..<0])) check res == expected test "importSql substr length no arg types checked": @@ -327,7 +318,7 @@ suite "string": # but sqlite doesn't really care... let res = query: select tb_string(substr(typstring, "1", 2)) - let expected = ss.mapIt(firstRunes(it, 2)) + let expected = ss.mapIt($(toRunes(it)[0..<2])) check res == expected let js = [ From 5f678724dd1d8810188d045810e511a63fd388b5 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 17:01:02 -0600 Subject: [PATCH 4/9] cleanup --- tools/ormin_importer.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/ormin_importer.nim b/tools/ormin_importer.nim index 92a31fb..d661042 100644 --- a/tools/ormin_importer.nim +++ b/tools/ormin_importer.nim @@ -6,6 +6,7 @@ import streams, strutils, os, parseopt, tables import db_connector/db_common import ./parsesql_tmp +import ../ormin/db_types #import compiler / [ast, renderer] From a13ed66ee1a48d557b6b0bb564519f691ddd00f8 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 17:09:27 -0600 Subject: [PATCH 5/9] fix postgres --- tests/tcommon.nim | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/tests/tcommon.nim b/tests/tcommon.nim index 9c6c387..21a72d2 100644 --- a/tests/tcommon.nim +++ b/tests/tcommon.nim @@ -169,11 +169,12 @@ suite "boolean": produce json check res == %*seqs.mapIt(%*{"typboolean": toBool(it)}) - test "importSql substr wrong type": - ## TODO: should this be allowed? - let res = query: - select tb_boolean(substr(typboolean, 1, 2)) - echo "res: ", res + when defined(sqlite): + test "importSql substr wrong type": + ## TODO: should this be allowed? + let res = query: + select tb_boolean(substr(typboolean, 1, 2)) + echo "res: ", res let fs = [-3.14, 2.56, 10.45] @@ -306,20 +307,21 @@ suite "string": select tb_string(replace(typstring, "e", "o")) check res == ss.mapIt(it.replace("e", "o")) - test "importSql substr length 0": - let res = query: - select tb_string(substr(typstring, 1, 0)) - let expected = ss.mapIt($(toRunes(it)[0..<0])) - check res == expected - - test "importSql substr length no arg types checked": - # TODO: fixme! - # the `functions` array only contains the types of the return - # but sqlite doesn't really care... - let res = query: - select tb_string(substr(typstring, "1", 2)) - let expected = ss.mapIt($(toRunes(it)[0..<2])) - check res == expected + when defined(sqlite): + test "importSql substr length 0": + let res = query: + select tb_string(substr(typstring, 1, 0)) + let expected = ss.mapIt($(toRunes(it)[0..<0])) + check res == expected + + test "importSql substr length no arg types checked": + # TODO: fixme! + # the `functions` array only contains the types of the return + # but sqlite doesn't really care... + let res = query: + select tb_string(substr(typstring, "1", 2)) + let expected = ss.mapIt($(toRunes(it)[0..<2])) + check res == expected let js = [ %*{"name": "tom", "age": 30}, From 38aff4155d11d862ad68a60310685ff33f2ee382 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sat, 20 Sep 2025 17:10:32 -0600 Subject: [PATCH 6/9] fix postgres --- tests/model_sqlite.sql | 6 +++++- tests/tcommon.nim | 29 ++++++++++++++--------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/model_sqlite.sql b/tests/model_sqlite.sql index 2c8a41d..bf99a06 100644 --- a/tests/model_sqlite.sql +++ b/tests/model_sqlite.sql @@ -15,6 +15,10 @@ create table if not exists tb_string( typstring text not null ); +create table if not exists tb_blob( + payload blob not null +); + create table if not exists tb_timestamp( dt1 timestamp not null, dt2 timestamp not null @@ -22,4 +26,4 @@ create table if not exists tb_timestamp( create table if not exists tb_json( typjson json not null -); \ No newline at end of file +); diff --git a/tests/tcommon.nim b/tests/tcommon.nim index 21a72d2..90c909e 100644 --- a/tests/tcommon.nim +++ b/tests/tcommon.nim @@ -307,21 +307,20 @@ suite "string": select tb_string(replace(typstring, "e", "o")) check res == ss.mapIt(it.replace("e", "o")) - when defined(sqlite): - test "importSql substr length 0": - let res = query: - select tb_string(substr(typstring, 1, 0)) - let expected = ss.mapIt($(toRunes(it)[0..<0])) - check res == expected - - test "importSql substr length no arg types checked": - # TODO: fixme! - # the `functions` array only contains the types of the return - # but sqlite doesn't really care... - let res = query: - select tb_string(substr(typstring, "1", 2)) - let expected = ss.mapIt($(toRunes(it)[0..<2])) - check res == expected + test "importSql substr length 0": + let res = query: + select tb_string(substr(typstring, 1, 0)) + let expected = ss.mapIt($(toRunes(it)[0..<0])) + check res == expected + + test "importSql substr length no arg types checked": + # TODO: fixme! + # the `functions` array only contains the types of the return + # but sqlite doesn't really care... + let res = query: + select tb_string(substr(typstring, "1", 2)) + let expected = ss.mapIt($(toRunes(it)[0..<2])) + check res == expected let js = [ %*{"name": "tom", "age": 30}, From 2acb87e37d689dd43104437a5637f9bdc2162b6e Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 21 Sep 2025 08:33:43 -0600 Subject: [PATCH 7/9] add docs --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 01cd9e0..6ddcdcf 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,21 @@ query: The tests include additional samples of JSON parameters and raw SQL expressions. +### Custom SQL Functions + +Use the `{.importSql.}` pragma to tell Ormin about additional SQL functions that your database provides. Declare a Nim proc or func that mirrors the SQL signature and mark it with the pragma; the declaration does not need an implementation because Ormin only uses it to register the function for the query DSL. + +```nim +proc substr(s: string; start, length: int): string {.importSql.} + +let rows = query: + select tb_string(substr(typstring, 1, 5)) +``` + +Imported functions participate in compile-time checking for arity and return type so they can be composed with regular Ormin expressions. + +**Limitation:** argument types are currently not validated, so using mismatched parameter types still compiles—ensure the arguments you pass match what the underlying SQL function expects. + ## Transactions and Batching TODO! From e2df8c2123dfb905eff6a7c917424f7d34fc0ee8 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 21 Sep 2025 08:37:12 -0600 Subject: [PATCH 8/9] add docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6ddcdcf..d541750 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,10 @@ Use the `{.importSql.}` pragma to tell Ormin about additional SQL functions that ```nim proc substr(s: string; start, length: int): string {.importSql.} +let name = "foo" let rows = query: select tb_string(substr(typstring, 1, 5)) + where substr(typstring, 1, 5) == ?name ``` Imported functions participate in compile-time checking for arity and return type so they can be composed with regular Ormin expressions. From d6bb334563e08ea506b876d556b35352a7490155 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Sun, 21 Sep 2025 08:39:14 -0600 Subject: [PATCH 9/9] bump version --- ormin.nimble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ormin.nimble b/ormin.nimble index 9e582f6..3be144b 100644 --- a/ormin.nimble +++ b/ormin.nimble @@ -1,6 +1,6 @@ # Package -version = "0.4.0" +version = "0.5.0" author = "Araq" description = "Prepared SQL statement generator. A lightweight ORM." license = "MIT"