Skip to content

Commit c057526

Browse files
authored
Query types with Option's support (#84)
* add return type support * add return type support * add return type support * add return type support * add return type support * cleanup * cleanup * add benchmark * move to faster hook types * move to faster hook types * move to faster hook types * move to faster hook types * check speed * cleanup * add fromQueryHook types * add fromQueryHook types * typed queries * typed queries * use monotimes * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * cleanup macros * simplify macros * simplify macros * simplify macros * simplify macros * reduce overhead * reduce overhead by using moves
1 parent 03c6335 commit c057526

8 files changed

Lines changed: 579 additions & 29 deletions

File tree

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,75 @@ let newId = query:
241241
242242
```
243243

244+
### Typed Queries
245+
246+
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.
247+
248+
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:
249+
250+
```nim
251+
type
252+
ThreadSummary = object
253+
id: int
254+
title: string
255+
256+
let threads = query(ThreadSummary):
257+
select thread(id, name as title)
258+
orderby id
259+
```
260+
261+
Selecting one column can map directly to a scalar type:
262+
263+
```nim
264+
let names = query(string):
265+
select thread(name)
266+
```
267+
268+
Queries that return a single row, such as a `limit 1` query, return one `T` instead of `seq[T]`:
269+
270+
```nim
271+
let thread = query(ThreadSummary):
272+
select thread(id, name as title)
273+
where id == ?threadId
274+
limit 1
275+
```
276+
277+
#### `fromQueryHook` Column Hooks
278+
279+
Typed queries deserialize each selected column through `fromQueryHook`. You can overload this hook for your own field or scalar destination types:
280+
281+
```nim
282+
import ormin/query_hooks
283+
284+
type
285+
TitleLength = distinct int
286+
287+
ThreadTitleSize = object
288+
id: int
289+
title: TitleLength
290+
291+
proc fromQueryHook*(val: var TitleLength, value: string) =
292+
val = TitleLength(value.len)
293+
294+
let rows = query(ThreadTitleSize):
295+
select thread(id, name as title)
296+
```
297+
298+
If a hook needs to handle SQL `NULL` itself, accept a `DbValue[SourceType]`:
299+
300+
```nim
301+
type
302+
NullableTitle = distinct string
303+
304+
proc fromQueryHook*(val: var NullableTitle, value: DbValue[string]) =
305+
if value.isNull:
306+
val = NullableTitle("<untitled>")
307+
else:
308+
val = NullableTitle(value.value)
309+
```
310+
311+
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.
312+
244313
### JSON and Raw SQL
245314

246315
JSON values can be spliced directly using `%` expressions. The `%` prefix tells Ormin to treat the following Nim expression as a `JsonNode` without conversion:

config.nims

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ task test, "Run all test suite":
1616

1717
exec "nim c -f -r tests/tfeature"
1818
exec "nim c -f -r tests/tcommon"
19+
exec "nim c -f -r -d:release tests/tquery_types"
1920
exec "nim c -f -r tests/tsqlite"
2021
exec "nim c -f -r tests/tdb_utils"
2122
exec "nim c -f -r tests/timportstatic"
@@ -34,6 +35,7 @@ task test_postgres, "Run PostgreSQL test suite":
3435

3536
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tfeature"
3637
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tcommon"
38+
exec "nim c -f -d:nimDebugDlOpen -r -d:release -d:postgre tests/tquery_types"
3739
exec "nim c -f -d:nimDebugDlOpen -r -d:postgre tests/tpostgre"
3840

3941
task buildexamples, "Build examples: chat and forum":

ormin.nimble

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Package
22

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

ormin/ormin_postgre.nim

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import strutils, db_connector/postgres, json, times
22

33
import db_connector/db_common
4+
import query_hooks
45
export db_common
56

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

66+
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
67+
parr[idx-1] = cstring(nil)
68+
6569
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
6670
t: typedesc) =
6771
let x = xx
@@ -129,9 +133,15 @@ proc fillString(dest: var string; src: cstring; srcLen: int) {.inline.} =
129133

130134
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
131135
t: typedesc; name: string) =
132-
let src = pqgetvalue(queryResult, queryI, idx.cint)
133-
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
134-
fillString(dest, src, srcLen)
136+
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
137+
when defined(nimNoNilSeqs):
138+
setLen(dest, 0)
139+
else:
140+
dest = nil
141+
else:
142+
let src = pqgetvalue(queryResult, queryI, idx.cint)
143+
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
144+
fillString(dest, src, srcLen)
135145

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

175+
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
176+
t: typedesc; name: string) =
177+
if pqgetisnull(queryResult, queryI, idx.cint) != 0:
178+
dest.isNull = true
179+
else:
180+
dest.isNull = false
181+
when T is string:
182+
let src = pqgetvalue(queryResult, queryI, idx.cint)
183+
let srcLen = int(pqgetlength(queryResult, queryI, idx.cint))
184+
fillString(dest.value, src, srcLen)
185+
else:
186+
bindResult(db, s, idx, dest.value, t, name)
187+
165188
template createJObject*(): untyped = newJObject()
166189
template createJArray*(): untyped = newJArray()
167190

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

200+
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
201+
pqgetisnull(queryResult, queryI, idx.cint) != 0
202+
177203
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
178204
t: typedesc; name: string) =
179205
{.error: "invalid type for JSON object".}

ormin/ormin_sqlite.nim

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import json, times
55

66
import db_connector/db_common
77
import db_connector/sqlite3
8+
import query_hooks
89
export db_common
910

1011
type
@@ -45,12 +46,12 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
4546
cast[pointer](nil)
4647
else:
4748
cast[pointer](unsafeAddr(xs[0]))
48-
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_STATIC) != SQLITE_OK:
49+
if bind_blob(s, idx.cint, blobPtr, xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
4950
dbError(db)
5051
elif t is int or t is int64 or t is bool:
5152
if bind_int64(s, idx.cint, x.int64) != SQLITE_OK: dbError(db)
5253
elif t is string:
53-
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_STATIC) != SQLITE_OK:
54+
if bind_text(s, idx.cint, cstring(x), x.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
5455
dbError(db)
5556
elif t is float64:
5657
if bind_double(s, idx.cint, x) != SQLITE_OK:
@@ -61,15 +62,19 @@ template bindParam*(db: DbConn; s: PStmt; idx: int; x, t: untyped) =
6162
else:
6263
x.utc().format("yyyy-MM-dd HH:mm:ss")
6364

64-
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_STATIC) != SQLITE_OK:
65+
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
6566
dbError(db)
6667
elif t is JsonNode:
6768
let xx = $x
68-
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_STATIC) != SQLITE_OK:
69+
if bind_text(s, idx.cint, cstring(xx), xx.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
6970
dbError(db)
7071
else:
7172
{.error: "type mismatch for query argument at position " & $idx.}
7273

74+
template bindNullParam*(db: DbConn; s: PStmt; idx: int) =
75+
if bind_null(s, idx.cint) != SQLITE_OK:
76+
dbError(db)
77+
7378
template bindParamJson*(db: DbConn; s: PStmt; idx: int; xx: JsonNode;
7479
t: typedesc) =
7580
let x = xx
@@ -86,7 +91,7 @@ template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
8691
t: typedesc[string]) =
8792
doAssert x.kind == JString
8893
let xs = x.str
89-
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_STATIC) != SQLITE_OK:
94+
if bind_text(s, idx.cint, cstring(xs), xs.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
9095
dbError(db)
9196

9297
template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
@@ -118,7 +123,7 @@ template bindFromJson*(db: DbConn; s: PStmt; idx: int; x: JsonNode;
118123
dtStr[0 ..< i]
119124
else:
120125
dtStr
121-
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_STATIC) != SQLITE_OK:
126+
if bind_text(s, idx.cint, cstring(dt), dt.len.cint, SQLITE_TRANSIENT) != SQLITE_OK:
122127
dbError(db)
123128

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

150155
template bindResult*(db: DbConn; s: PStmt; idx: int; dest: var string;
151156
t: typedesc; name: string) =
152-
let srcLen = column_bytes(s, idx.cint)
153-
let src = column_text(s, idx.cint)
154-
fillString(dest, src, srcLen)
157+
if column_type(s, idx.cint) == SQLITE_NULL:
158+
when defined(nimNoNilSeqs):
159+
setLen(dest, 0)
160+
else:
161+
dest = nil
162+
else:
163+
let srcLen = column_bytes(s, idx.cint)
164+
let src = column_text(s, idx.cint)
165+
fillString(dest, src, srcLen)
155166

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

196+
template bindResult*[T](db: DbConn; s: PStmt; idx: int; dest: var DbValue[T];
197+
t: typedesc; name: string) =
198+
if column_type(s, idx.cint) == SQLITE_NULL:
199+
dest.isNull = true
200+
else:
201+
dest.isNull = false
202+
when T is string:
203+
let srcLen = column_bytes(s, idx.cint)
204+
let src = column_text(s, idx.cint)
205+
fillString(dest.value, src, srcLen)
206+
else:
207+
bindResult(db, s, idx, dest.value, t, name)
208+
185209
template createJObject*(): untyped = newJObject()
186210
template createJArray*(): untyped = newJArray()
187211

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

221+
template columnIsNull*(db: DbConn; s: PStmt; idx: int): bool =
222+
column_type(s, idx.cint) == SQLITE_NULL
223+
197224
template bindToJson*(db: DbConn; s: PStmt; idx: int; obj: JsonNode;
198225
t: typedesc; name: string) =
199226
{.error: "invalid type for JSON object".}

0 commit comments

Comments
 (0)