Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
58afe70
add return type support
elcritch Apr 20, 2026
c9d9007
add return type support
elcritch Apr 20, 2026
699cd0b
add return type support
elcritch Apr 21, 2026
6786b13
add return type support
elcritch Apr 21, 2026
c8908e9
add return type support
elcritch Apr 21, 2026
eec4ce6
cleanup
elcritch Apr 21, 2026
fd76ba7
cleanup
elcritch Apr 21, 2026
3cc5e1a
add benchmark
elcritch Apr 23, 2026
1cdaef3
move to faster hook types
elcritch Apr 24, 2026
8bb0d52
move to faster hook types
elcritch Apr 24, 2026
ccfc944
move to faster hook types
elcritch Apr 24, 2026
be421e9
move to faster hook types
elcritch Apr 24, 2026
e6f6754
check speed
elcritch Apr 25, 2026
8936f68
cleanup
elcritch Apr 25, 2026
d354729
add fromQueryHook types
elcritch Apr 25, 2026
8c7bf32
add fromQueryHook types
elcritch Apr 25, 2026
4c13acb
typed queries
elcritch Apr 25, 2026
73699e2
typed queries
elcritch Apr 25, 2026
9ea8fdb
use monotimes
elcritch Apr 25, 2026
a6d4f61
cleanup macros
elcritch Apr 25, 2026
12432a7
cleanup macros
elcritch Apr 25, 2026
c84c950
cleanup macros
elcritch Apr 25, 2026
3e4cb6d
cleanup macros
elcritch Apr 25, 2026
d1843d7
cleanup macros
elcritch Apr 25, 2026
a3e75a9
cleanup macros
elcritch Apr 25, 2026
d700b8a
cleanup macros
elcritch Apr 25, 2026
787169a
cleanup macros
elcritch Apr 25, 2026
e410728
cleanup macros
elcritch Apr 25, 2026
7d46a8c
cleanup macros
elcritch Apr 25, 2026
e2286e7
cleanup macros
elcritch Apr 25, 2026
71d87cd
cleanup macros
elcritch Apr 25, 2026
8957475
cleanup macros
elcritch Apr 25, 2026
a880c0a
cleanup macros
elcritch Apr 26, 2026
4893397
cleanup macros
elcritch Apr 26, 2026
e924c7e
simplify macros
elcritch Apr 26, 2026
91ae2a2
simplify macros
elcritch Apr 26, 2026
b75a67b
simplify macros
elcritch Apr 26, 2026
12d09a5
simplify macros
elcritch Apr 26, 2026
50b5a7d
reduce overhead
elcritch Apr 26, 2026
43cc2fc
reduce overhead by using moves
elcritch Apr 26, 2026
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
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,75 @@ let newId = query:

```

### Typed Queries

Use `query(T):` when you want Ormin to deserialize selected columns directly into a named Nim type instead of returning the default tuple shape. This is useful at module boundaries where a named object, ref object, or scalar domain type is clearer than a tuple.

For object results, selected column names must match fields on the destination type. Use `as` aliases when the database column name differs from the Nim field name:

```nim
type
ThreadSummary = object
id: int
title: string

let threads = query(ThreadSummary):
select thread(id, name as title)
orderby id
```

Selecting one column can map directly to a scalar type:

```nim
let names = query(string):
select thread(name)
```

Queries that return a single row, such as a `limit 1` query, return one `T` instead of `seq[T]`:

```nim
let thread = query(ThreadSummary):
select thread(id, name as title)
where id == ?threadId
limit 1
```

#### `fromQueryHook` Column Hooks

Typed queries deserialize each selected column through `fromQueryHook`. You can overload this hook for your own field or scalar destination types:

```nim
import ormin/query_hooks

type
TitleLength = distinct int

ThreadTitleSize = object
id: int
title: TitleLength

proc fromQueryHook*(val: var TitleLength, value: string) =
val = TitleLength(value.len)

let rows = query(ThreadTitleSize):
select thread(id, name as title)
```

If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`:

```nim
type
NullableTitle = distinct string

proc fromQueryHook*(val: var NullableTitle, value: DbValue[string]) =
if value.isNull:
val = NullableTitle("<untitled>")
else:
val = NullableTitle(value.value)
```

These are column deserialization hooks. In object typed queries, Ormin calls `fromQueryHook` separately for each selected column that maps to a destination field; it does not currently call a hook for the entire row object. For whole-row transformations, query into an intermediate typed result and convert it in regular Nim code.

### JSON and Raw SQL

JSON values can be spliced directly using `%` expressions. The `%` prefix tells Ormin to treat the following Nim expression as a `JsonNode` without conversion:
Expand Down
2 changes: 2 additions & 0 deletions config.nims
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ task test, "Run all test suite":

exec "nim c -f -r tests/tfeature"
exec "nim c -f -r tests/tcommon"
exec "nim c -f -r -d:release tests/tquery_types"
exec "nim c -f -r tests/tsqlite"
exec "nim c -f -r tests/tdb_utils"
exec "nim c -f -r tests/timportstatic"
Expand All @@ -34,6 +35,7 @@ task test_postgres, "Run PostgreSQL test suite":

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:release -d:postgre tests/tquery_types"
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre"

task buildexamples, "Build examples: chat and forum":
Expand Down
3 changes: 2 additions & 1 deletion ormin.nimble
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Package

version = "0.8.1"
version = "0.9.0"
author = "Araq"
description = "Prepared SQL statement generator. A lightweight ORM."
license = "MIT"
Expand All @@ -20,3 +20,4 @@ feature "examples":
import std/os
when fileExists("config.nims"):
include "config.nims"

32 changes: 29 additions & 3 deletions ormin/ormin_postgre.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import strutils, db_connector/postgres, json, times

import db_connector/db_common
import query_hooks
export db_common

type
Expand Down Expand Up @@ -62,6 +63,9 @@ template bindParamUnchecked(db: DbConn; s: PStmt; idx: int; x: untyped; t: untyp
pparams[idx-1] = $x
parr[idx-1] = cstring(pparams[idx-1])

template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
parr[idx-1] = cstring(nil)

template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
Expand Down Expand Up @@ -129,9 +133,15 @@ proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} =

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest, src, srcLen)
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest, src, srcLen)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: float64;
t: typedesc; name: string) =
Expand Down Expand Up @@ -162,6 +172,19 @@ template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
t: typedesc; name: string) =
dest = parseJson($pqgetvalue(queryResult, queryI, idx.cint))

template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let src = pqgetvalue(queryResult, queryI, idx.cint)
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)

template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()

Expand All @@ -174,6 +197,9 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
else:
bindToJson(db, s, idx, x, t, name)

template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
pqgetisnull(queryResult, queryI, idx.cint) != 0

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
Expand Down
45 changes: 36 additions & 9 deletions ormin/ormin_sqlite.nim
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import json, times

import db_connector/db_common
import db_connector/sqlite3
import query_hooks
export db_common

type
Expand Down Expand Up @@ -45,12 +46,12 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
cast[pointer](nil)
else:
cast[pointer](unsafeAddr(xs[0]))
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is int or t is int64 or t is bool:
if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
elif t is string:
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is float64:
if bind_double(s, idx.cint, x) != SQLITE_OK:
Expand All @@ -61,15 +62,19 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
else:
x.utc().format("yyyy-MM-dd HH:mm:ss")

if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
elif t is JsonNode:
let xx = $x
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)
else:
{.error: "type mismatch for query argument at position " & $idx.}

template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
if bind_null(s, idx.cint) != SQLITE_OK:
dbError(db)

template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
t: typedesc) =
let x = xx
Expand All @@ -86,7 +91,7 @@ template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
t: typedesc[string]) =
doAssert x.kind == JString
let xs = x.str
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)

template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
Expand Down Expand Up @@ -118,7 +123,7 @@ template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
dtStr[0 ..< i]
else:
dtStr
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_STATIC) != SQLITE_OK:
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
dbError(db)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: int;
Expand Down Expand Up @@ -149,9 +154,15 @@ proc fillBytes(dest: var seq[byte]; src: pointer; srcLen: int) =

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
t: typedesc; name: string) =
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest, src, srcLen)
if column_type(s, idx.cint) == SQLITE_NULL:
when defined(nimNoNilSeqs):
setLen(dest, 0)
else:
dest = nil
else:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest, src, srcLen)

template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var blobType;
t: typedesc; name: string) =
Expand Down Expand Up @@ -182,6 +193,19 @@ template bindResult*(db: DbConn; s: PStmt; idx: int; dest: JsonNode;
let src = column_text(s, idx.cint)
dest = parseJson($src)

template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
t: typedesc; name: string) =
if column_type(s, idx.cint) == SQLITE_NULL:
dest.isNull = true
else:
dest.isNull = false
when T is string:
let srcLen = column_bytes(s, idx.cint)
let src = column_text(s, idx.cint)
fillString(dest.value, src, srcLen)
else:
bindResult(db, s, idx, dest.value, t, name)

template createJObject*(): untyped = newJObject()
template createJArray*(): untyped = newJArray()

Expand All @@ -194,6 +218,9 @@ template bindResultJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
else:
bindToJson(db, s, idx, x, t, name)

template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
column_type(s, idx.cint) == SQLITE_NULL

template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
t: typedesc; name: string) =
{.error: "invalid type for JSON object".}
Expand Down
Loading
Loading