Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
*
!*/
!*.*
*.db
*.js
nimcache
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,23 @@ 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 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.

**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!
Expand Down
2 changes: 1 addition & 1 deletion ormin.nimble
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
43 changes: 43 additions & 0 deletions ormin/db_types.nim
Original file line number Diff line number Diff line change
@@ -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
75 changes: 75 additions & 0 deletions ormin/queries.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "="
Expand Down Expand Up @@ -37,6 +39,79 @@ var
Function(name: "replace", arity: 3, typ: dbVarchar)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be nice to upgrade these at some point to also support type checking the args. But alas, not today.

]

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)
Copy link
Contributor Author

@elcritch elcritch Sep 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used GPT5-Codex-High for this. Seems to have done a good job. It checked for conditions I wouldn't have thought to check. I had to cleanup a little bit in the tests.

let params = n[3]
expectKind(params, nnkFormalParams)
var paramCount = 0
var hasVarargs = false
for i in 1..<params.len:
let identDefs = params[i]
expectKind(identDefs, nnkIdentDefs)
if identDefs.len < 3:
macros.error("unexpected parameter definition in {.importSql.}", identDefs)
let defaultValue = identDefs[^1]
if defaultValue.kind != nnkEmpty:
macros.error("{.importSql.} procs cannot declare default values", defaultValue)
let typeNode = identDefs[^2]
if isVarargsType(typeNode):
if hasVarargs:
macros.error("{.importSql.} supports only a single varargs parameter", identDefs)
if params.len > 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]]
Expand Down
24 changes: 22 additions & 2 deletions tests/tcommon.nim
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ let
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / sqlFileName)

proc substr(s: string; start, length: int): string {.importSql.}

let
min = -3
Expand Down Expand Up @@ -170,6 +171,12 @@ suite "boolean":
produce json
check res == %*seqs.mapIt(%*{"typboolean": toBool(it)})

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]

Expand Down Expand Up @@ -223,7 +230,6 @@ suite "float":
select tb_float(abs(typfloat))
check res == fs.mapIt(abs(it))


let ss = ["one", "Two", "three", "第四", "four'th"]

suite "string_insert":
Expand Down Expand Up @@ -302,7 +308,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($(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},
Expand Down
39 changes: 2 additions & 37 deletions tools/ormin_importer.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import streams, strutils, os, parseopt, tables
import db_connector/db_common

import ../ormin/db_types
import ../ormin/parsesql_tmp

#import compiler / [ast, renderer]
Expand Down Expand Up @@ -70,43 +71,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) =
Expand Down