Add Ormin ORM support for BaraDB (Nim client)
CI / test (push) Has been cancelled
CI / verify (push) Has been cancelled
Clients CI / build-server (push) Has been cancelled
Clients CI / test-python (push) Has been cancelled
Clients CI / test-javascript (push) Has been cancelled
Clients CI / test-nim (push) Has been cancelled
Clients CI / test-rust (push) Has been cancelled

This commit is contained in:
2026-05-15 21:49:08 +03:00
parent 2e945c1dcb
commit 4e54075078
63 changed files with 9764 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
include system/inclrtl
import macros
proc transLastStmt(n, res, bracketExpr: NimNode): (NimNode, NimNode, NimNode) =
case n.kind
of nnkIfExpr, nnkIfStmt, nnkTryStmt, nnkCaseStmt:
result[0] = copyNimTree(n)
result[1] = copyNimTree(n)
result[2] = copyNimTree(n)
for i in ord(n.kind == nnkCaseStmt)..<n.len:
(result[0][i], result[1][^1], result[2][^1]) = transLastStmt(n[i], res, bracketExpr)
of nnkStmtList, nnkStmtListExpr, nnkBlockStmt, nnkBlockExpr, nnkWhileStmt,
nnkForStmt, nnkElifBranch, nnkElse, nnkElifExpr, nnkOfBranch, nnkExceptBranch:
result[0] = copyNimTree(n)
result[1] = copyNimTree(n)
result[2] = copyNimTree(n)
if n.len >= 1:
(result[0][^1], result[1][^1], result[2][^1]) = transLastStmt(n[^1], res, bracketExpr)
of nnkTableConstr:
result[1] = n[0][0]
result[2] = n[0][1]
if bracketExpr.len == 1:
bracketExpr.add([newCall(bindSym"typeof", newEmptyNode()), newCall(
bindSym"typeof", newEmptyNode())])
template adder(res, k, v) = res[k] = v
result[0] = getAst(adder(res, n[0][0], n[0][1]))
of nnkCurly:
result[2] = n[0]
if bracketExpr.len == 1:
bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
template adder(res, v) = res.incl(v)
result[0] = getAst(adder(res, n[0]))
else:
result[2] = n
if bracketExpr.len == 1:
bracketExpr.add(newCall(bindSym"typeof", newEmptyNode()))
template adder(res, v) = res.add(v)
result[0] = getAst(adder(res, n))
macro collect*(init, body: untyped): untyped =
let res = genSym(nskVar, "collectResult")
expectKind init, {nnkCall, nnkIdent, nnkSym}
let bracketExpr = newTree(nnkBracketExpr,
if init.kind == nnkCall: init[0] else: init)
let (resBody, keyType, valueType) = transLastStmt(body, res, bracketExpr)
if bracketExpr.len == 3:
bracketExpr[1][1] = keyType
bracketExpr[2][1] = valueType
else:
bracketExpr[1][1] = valueType
let call = newTree(nnkCall, bracketExpr)
if init.kind == nnkCall:
for i in 1 ..< init.len:
call.add init[i]
result = newTree(nnkStmtListExpr, newVarStmt(res, call), resBody, res)
+1
View File
@@ -0,0 +1 @@
--path:"$projectDir/../../ormin"
@@ -0,0 +1,26 @@
-- 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
);
create table "UPPER_QUOTED" (
id integer primary key
);
create table "A""B" (
id integer primary key
);
@@ -0,0 +1,60 @@
create table if not exists thread(
id serial primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default CURRENT_TIMESTAMP
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id serial primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default CURRENT_TIMESTAMP,
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default CURRENT_TIMESTAMP,
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id serial primary key,
author integer not null,
ip varchar(20) not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default CURRENT_TIMESTAMP,
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id serial primary key,
ip varchar(20) not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default CURRENT_TIMESTAMP,
foreign key (userid) references person(id)
);
create table if not exists antibot(
id serial primary key,
ip varchar(20) not null,
answer varchar(30) not null,
created timestamp not null default CURRENT_TIMESTAMP
);
create table if not exists error(
uuid varchar(36) primary key,
message varchar(1000) not null,
created timestamp not null default CURRENT_TIMESTAMP
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
@@ -0,0 +1,62 @@
pragma foreign_keys = on;
create table if not exists thread(
id integer primary key,
name varchar(100) not null,
views integer not null,
modified timestamp not null default (DATETIME('now'))
);
create unique index if not exists ThreadNameIx on thread (name);
create table if not exists person(
id integer primary key,
name varchar(20) not null,
password varchar(32) not null,
email varchar(30) not null,
creation timestamp not null default (DATETIME('now')),
salt varchar(128) not null,
status varchar(30) not null,
lastOnline timestamp not null default (DATETIME('now')),
ban varchar(128) not null default ''
);
create unique index if not exists UserNameIx on person (name);
create table if not exists post(
id integer primary key,
author integer not null,
ip varchar(20) not null,
header varchar(100) not null,
content varchar(1000) not null,
thread integer not null,
creation timestamp not null default (DATETIME('now')),
foreign key (thread) references thread(id),
foreign key (author) references person(id)
);
create table if not exists session(
id integer primary key,
ip varchar(20) not null,
password varchar(32) not null,
userid integer not null,
lastModified timestamp not null default (DATETIME('now')),
foreign key (userid) references person(id)
);
create table if not exists antibot(
id integer primary key,
ip varchar(20) not null,
answer varchar(30) not null,
created timestamp not null default (DATETIME('now'))
);
create table if not exists error(
uuid varchar(36) primary key,
message varchar(1000) not null,
created timestamp not null default (DATETIME('now'))
);
create index PersonStatusIdx on person(status);
create index PostByAuthorIdx on post(thread, author);
+45
View File
@@ -0,0 +1,45 @@
create table if not exists tb_serial(
typserial serial primary key,
typinteger integer not null
);
create table if not exists tb_boolean(
typboolean boolean not null
);
create table if not exists tb_float(
typfloat real not null
);
create table if not exists tb_string(
typstring text not null
);
create table if not exists tb_timestamp(
dt timestamp not null,
dtn timestamptz not null,
dtz timestamptz not null
);
create table if not exists tb_json(
typjson json not null
);
create table if not exists tb_composite_pk(
pk1 integer not null,
pk2 integer not null,
message text not null,
primary key (pk1, pk2)
);
create table if not exists tb_composite_fk(
id integer not null,
fk1 integer not null,
fk2 integer not null,
foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2)
);
create table if not exists tb_nullable(
id integer primary key,
note text null
);
+48
View File
@@ -0,0 +1,48 @@
create table if not exists tb_serial(
typserial integer primary key,
typinteger integer not null
);
create table if not exists tb_boolean(
typboolean boolean not null
);
create table if not exists tb_float(
typfloat real not null
);
create table if not exists tb_string(
typstring text not null
);
create table if not exists tb_timestamp(
dt1 timestamp not null,
dt2 timestamp not null
);
create table if not exists tb_json(
typjson json not null
);
create table if not exists tb_composite_pk(
pk1 integer not null,
pk2 integer not null,
message text not null,
primary key (pk1, pk2)
);
create table if not exists tb_composite_fk(
id integer not null,
fk1 integer not null,
fk2 integer not null,
foreign key (fk1, fk2) references tb_composite_pk(pk1, pk2)
);
create table if not exists tb_blob(
id integer primary key,
typblob blob not null
);
create table if not exists tb_nullable(
id integer primary key,
note text null
);
@@ -0,0 +1,4 @@
create table user_static (
id integer primary key,
name varchar(255) not null
);
+508
View File
@@ -0,0 +1,508 @@
import unittest, json, strutils, strformat, sequtils, macros, times, os, math, unicode
import ormin
import ormin/db_utils
when defined(postgre):
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
import db_connector/db_postgres as db_postgres
const backend = DbBackend.postgre
importModel(backend, "model_postgre")
const sqlFileName = "model_postgre.sql"
let db {.global.} = db_postgres.open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "model_sqlite")
const sqlFileName = "model_sqlite.sql"
# let db {.global.} = open(":memory:", "", "", "")
let db {.global.} = open("test.db", "", "", "")
let
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / sqlFileName)
proc substr(s: string; start, length: int): string {.importSql.}
let
min = -3
max = 3
serial_int_seqs = [(1, min), (2, 1), (3, 2), (4, max)]
suite &"Test common database types and functions of {backend}":
discard
suite "serial_insert":
setup:
db.dropTable(sqlFile, "tb_serial")
db.createTable(sqlFile, "tb_serial")
test "insert":
for i in serial_int_seqs:
query:
insert tb_serial(typinteger = ?i[1])
check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
test "json":
for i in serial_int_seqs:
let v = %*{"typinteger": i[1]}
query:
insert tb_serial(typinteger = %v["typinteger"])
check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
suite "serial":
db.dropTable(sqlFile, "tb_serial")
db.createTable(sqlFile, "tb_serial")
let insertSql = sql"insert into tb_serial(typinteger) values (?)"
for (_, v) in serial_int_seqs:
db.exec(insertSql, v)
doAssert db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len
test "query":
let res = query:
select tb_serial(typserial, typinteger)
check res == serial_int_seqs
test "where":
let
id = 1
res = query:
select tb_serial(typserial, typinteger)
where typserial == ?id
check res == serial_int_seqs.filterIt(it[0] == id)
test "json":
let res = query:
select tb_serial(typserial, typinteger)
produce json
check res == %*serial_int_seqs.mapIt(%*{"typserial": it[0], "typinteger": it[1]})
test "count":
let res = query:
select tb_serial(count(_))
check res[0] == serial_int_seqs.len
test "sum":
let res = query:
select tb_serial(sum(typinteger))
check res[0] == serial_int_seqs.mapIt(it[1]).sum()
test "avg":
let res = query:
select tb_serial(avg(typinteger))
check res[0] == serial_int_seqs.mapIt(it[1]).sum() / serial_int_seqs.len()
test "min":
let res = query:
select tb_serial(min(typinteger))
check res[0] == min
test "max":
let res = query:
select tb_serial(max(typinteger))
check res[0] == max
test "abs":
let res = query:
select tb_serial(abs(typinteger))
check res == serial_int_seqs.mapIt(abs(it[1]))
let seqs = toSeq(1..3)
suite "boolean_insert":
setup:
db.dropTable(sqlFile, "tb_boolean")
db.createTable(sqlFile, "tb_boolean")
test "insert":
for i in seqs:
let b = (i mod 2 == 0)
query:
insert tb_boolean(typboolean = ?b)
check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
test "json":
for i in seqs:
let b = %*{"typboolean": (i mod 2 == 0)}
query:
insert tb_boolean(typboolean = %b["typboolean"])
check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
suite "boolean":
db.dropTable(sqlFile, "tb_boolean")
db.createTable(sqlFile, "tb_boolean")
proc toBool(x: int): bool =
if x mod 2 == 0: true else: false
proc toDbValue(x: int): auto =
when defined(postgre):
if x mod 2 == 0: "t" else: "f"
else:
if x mod 2 == 0: 1 else: 0
let insertSql = sql"insert into tb_boolean(typboolean) values (?)"
for i in seqs:
db.exec(insertSql, toDbValue(i))
doAssert db.getValue(sql"select count(*) from tb_boolean") == $seqs.len
test "query":
let res = query:
select tb_boolean(typboolean)
check res == seqs.mapIt(it mod 2 == 0)
test "where":
let
b = true
res = query:
select tb_boolean(typboolean)
where typboolean == ?b
check res == seqs.map(toBool).filterIt(it == b)
test "json":
let res = query:
select tb_boolean(typboolean)
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]
suite "float_insert":
setup:
db.dropTable(sqlFile, "tb_float")
db.createTable(sqlFile, "tb_float")
test "insert":
for f in fs:
query:
insert tb_float(typfloat = ?f)
check db.getValue(sql"select count(*) from tb_float") == $fs.len
test "json":
for f in fs:
let v = %*{"typfloat": f}
query:
insert tb_float(typfloat = %v["typfloat"])
check db.getValue(sql"select count(*) from tb_float") == $fs.len
suite "float":
db.dropTable(sqlFile, "tb_float")
db.createTable(sqlFile, "tb_float")
let insertSql = sql"insert into tb_float(typfloat) values (?)"
for f in fs:
db.exec(insertSql, f)
doAssert db.getValue(sql"select count(*) from tb_float") == $fs.len
test "query":
let res = query:
select tb_float(typfloat)
check res == fs
test "where":
let res = query:
select tb_float(typfloat)
where typfloat == ?fs[0]
check res == [fs[0]]
test "json":
let res = query:
select tb_float(typfloat)
produce json
check res == %*fs.mapIt(%*{"typfloat": it})
test "abs":
let res = query:
select tb_float(abs(typfloat))
check res == fs.mapIt(abs(it))
let ss = ["one", "Two", "three", "第四", "four'th"]
suite "string_insert":
setup:
db.dropTable(sqlFile, "tb_string")
db.createTable(sqlFile, "tb_string")
test "insert":
for v in ss:
query:
insert tb_string(typstring = ?v)
check db.getValue(sql"select count(*) from tb_string") == $ss.len
test "insert":
for v in ss:
query:
insert tb_string(typstring = "can't touch this")
check db.getValue(sql"select count(*) from tb_string") == $ss.len
test "json":
for v in ss:
let j = %*{"typstring": v}
query:
insert tb_string(typstring = %j["typstring"])
check db.getValue(sql"select count(*) from tb_string") == $ss.len
suite "string":
db.dropTable(sqlFile, "tb_string")
db.createTable(sqlFile, "tb_string")
let insertsql = sql"insert into tb_string(typstring) values (?)"
for v in ss:
db.exec(insertsql, v)
doAssert db.getValue(sql"select count(*) from tb_string") == $ss.len
test "query":
let res = query:
select tb_string(typstring)
check res == ss
test "where":
let res = query:
select tb_string(typstring)
where typstring == ?ss[0]
check res[0] == ss[0]
test "json":
let res = query:
select tb_string(typstring)
produce json
check res == %*ss.mapIt(%*{"typstring": it})
test "concat_op":
let
s = "typstring: "
let res = query:
select tb_string(?s & typstring)
check res == ss.mapIt(s & it)
test "length":
let res = query:
select tb_string(length(typstring))
check res == ss.mapIt(it.runeLen)
test "lower":
let res = query:
select tb_string(lower(typstring))
check res == ss.mapIt(it.toLowerAscii)
test "upper":
let res = query:
select tb_string(upper(typstring))
check res == ss.mapIt(it.toUpperAscii)
test "replace":
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},
%*{"name": "bob", "age": 35},
%*{"name": "jack", "age": 23}
]
suite "insert_json":
setup:
db.dropTable(sqlFile, "tb_json")
db.createTable(sqlFile, "tb_json")
test "insert":
for j in js:
query:
insert tb_json(typjson = ?j)
check db.getValue(sql"select count(*) from tb_json") == $js.len
suite "json":
db.dropTable(sqlFile, "tb_json")
db.createTable(sqlFile, "tb_json")
let insertSql = sql"insert into tb_json(typjson) values (?)"
for j in js:
db.exec(insertSql, j)
doAssert db.getValue(sql"select count(*) from tb_json") == $js.len
test "query":
let res = query:
select tb_json(typjson)
check res == js
test "json":
let res = query:
select tb_json(typjson)
produce json
check res == %*js.mapIt(%*{"typjson": it})
let cps = [
(1, 1, "one-one"),
(1, 2, "one-two"),
(2, 1, "two-one")
]
suite "composite_pk_insert":
setup:
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
test "insert":
for r in cps:
query:
insert tb_composite_pk(pk1 = ?r[0], pk2 = ?r[1], message = ?r[2])
check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
test "json":
for r in cps:
let v = %*{"pk1": r[0], "pk2": r[1], "message": r[2]}
query:
insert tb_composite_pk(pk1 = %v["pk1"], pk2 = %v["pk2"], message = %v["message"])
check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
suite "composite_pk":
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
let insertSql = sql"insert into tb_composite_pk(pk1, pk2, message) values (?, ?, ?)"
for r in cps:
db.exec(insertSql, r[0], r[1], r[2])
doAssert db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len
test "query":
let res = query:
select tb_composite_pk(pk1, pk2, message)
check res == cps
test "where":
let res = query:
select tb_composite_pk(pk1, pk2, message)
where pk1 == ?cps[0][0]
check res == cps.filterIt(it[0] == cps[0][0])
test "json":
let res = query:
select tb_composite_pk(pk1, pk2, message)
produce json
check res == %*cps.mapIt(%*{"pk1": it[0], "pk2": it[1], "message": it[2]})
suite "nullable":
setup:
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
query:
insert tb_nullable(id = 1, note = nil)
query:
insert tb_nullable(id = 2, note = "hello")
test "where_null":
let res = query:
select tb_nullable(id)
where note == nil
check res == @[1]
test "where_not_null":
let res = query:
select tb_nullable(id)
where note != null
check res == @[2]
test "update_to_null":
query:
update tb_nullable(note = null)
where id == 2
let res = query:
select tb_nullable(id)
where note == nil
check res == @[1, 2]
test "json_null":
let res = query:
select tb_nullable(id, note)
orderby id
produce json
check res[0]["note"].kind == JNull
check res[1]["note"].getStr == "hello"
suite "upsert":
setup:
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
test "onconflict do nothing":
let first = "first value"
query:
insert tb_nullable(id = 1, note = ?first)
let ignored = "should not overwrite"
query:
insert tb_nullable(id = 1, note = ?ignored)
onconflict(id)
donothing()
let note = db.getValue(sql"select note from tb_nullable where id = 1")
check note == first
test "onconflict do update":
let first = "old note"
query:
insert tb_nullable(id = 2, note = ?first)
let replacement = "new note"
query:
insert tb_nullable(id = 2, note = ?replacement)
onconflict(id)
doupdate(note = ?replacement)
let note = db.getValue(sql"select note from tb_nullable where id = 2")
check note == replacement
test "onconflict do update where":
query:
insert tb_nullable(id = 3, note = "keep-me")
query:
insert tb_nullable(id = 3, note = "replace-me")
onconflict(id)
doupdate(note = "replace-me")
where note != "keep-me"
let note = db.getValue(sql"select note from tb_nullable where id = 3")
check note == "keep-me"
static:
doAssert not compiles(
(block:
query:
insert tb_nullable(id = 100, note = "x")
where id == 100
)
)
+166
View File
@@ -0,0 +1,166 @@
import unittest, os, sequtils
import db_connector/db_common
from db_connector/db_sqlite import open, exec, getValue
import ormin/db_utils
const usesLegacySqliteDropNames = defined(sqlite) and defined(orminLegacySqliteDropNames)
let
db {.global.} = open(":memory:", "", "", "")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "db_utils_case_quoted.sql")
let sqlContent = """
-- 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
);
create table "UPPER_QUOTED" (
id integer primary key
);
create table "A""B" (
id integer primary key
);
"""
const staticSqlContent = staticLoad("db_utils_case_quoted.sql")
writeFile($sqlFile, sqlContent)
suite "db_utils: case and quoted names":
test "staticLoad returns typed compile-time SQL":
let loaded: DbSql = staticSqlContent
check $loaded == sqlContent
test "quoteDbIdentifier quotes and escapes identifiers":
check $quoteDbIdentifier("lower_table") == "lower_table"
check $quoteDbIdentifier("UPPER_TABLE") == "upper_table"
check $quoteDbIdentifier("\"Quoted Table\"") == "\"Quoted Table\""
check $quoteDbIdentifier("`Quoted Table2`") == "\"Quoted Table2\""
check $quoteDbIdentifier("'Quoted Table3'") == "\"Quoted Table3\""
check $quoteDbIdentifier("\"UPPER_TABLE\"") == "\"UPPER_TABLE\""
check $quoteDbIdentifier("\"A\"\"B\"") == "\"A\"\"B\""
check $quoteDbIdentifier("schema.table") == "schema.table"
check $quoteDbIdentifier("Schema.\"Mixed Table\"") == "schema.\"Mixed Table\""
check $quoteDbIdentifier("weird\"name") == "\"weird\"\"name\""
test "check tables names":
let pairs = tablePairs(sqlContent).toSeq()
check pairs.len == 6
check pairs[0][0] == ("lower_table")
check pairs[1][0] == ("upper_table")
check pairs[2][0] == ("quoted table")
check pairs[3][0] == ("quoted table2")
check pairs[4][0] == ("upper_quoted")
check pairs[5][0] == ("a\"b")
check pairs[0][1] == "create table lower_table(id integer primary key );"
check pairs[1][1] == "create table UPPER_TABLE(id integer primary key );"
check pairs[2][1] == "create table \"Quoted Table\"(id integer primary key );"
check pairs[4][1] == "create table \"UPPER_QUOTED\"(id integer primary key );"
check pairs[5][1] == "create table \"A\"\"B\"(id integer primary key );"
test "tablePairs parses compile-time SQL":
let pairs = tablePairs(staticSqlContent).toSeq()
check pairs.len == 6
check pairs[0][0] == "lower_table"
check pairs[1][0] == "upper_table"
check pairs[2][0] == "quoted table"
check pairs[3][0] == "quoted table2"
check pairs[4][0] == "upper_quoted"
check pairs[5][0] == "a\"b"
test "createTable creates all tables from SQL file":
db.createTable(sqlFile)
let countAll = db.getValue(sql"select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table')")
check countAll == "3"
test "createTable with specific lowercased name matches quoted":
# Use a new in-memory DB for isolation
let db2 = open(":memory:", "", "", "")
db2.createTable(sqlFile, "quoted table")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table'")
check countQuoted == "1"
test "createTable creates all tables from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent)
let countAll = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table','Quoted Table2','UPPER_QUOTED','A\"B')"))
check countAll == "6"
test "createTable with specific lowercased name matches quoted from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent, "quoted table")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table'")
check countQuoted == "1"
test "dropTable handles quoted names from SQL file":
let db2 = open(":memory:", "", "", "")
db2.createTable(sqlFile)
db2.dropTable(sqlFile, "quoted table2")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table2'")
when usesLegacySqliteDropNames:
check countQuoted == "1"
else:
check countQuoted == "0"
db2.dropTable(sqlFile, "upper_quoted")
let countUpperQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'UPPER_QUOTED'")
when usesLegacySqliteDropNames:
check countUpperQuoted == "1"
else:
check countUpperQuoted == "0"
db2.dropTable(sqlFile, "a\"b")
let countEscapedQuoted = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name = 'A\"B'"))
when usesLegacySqliteDropNames:
check countEscapedQuoted == "1"
else:
check countEscapedQuoted == "0"
test "dropTable removes tables from compile-time SQL":
let db2 = open(":memory:", "", "", "")
db2.createTable(staticSqlContent)
db2.dropTable(staticSqlContent, "quoted table2")
let countQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'Quoted Table2'")
when usesLegacySqliteDropNames:
check countQuoted == "1"
else:
check countQuoted == "0"
db2.dropTable(staticSqlContent, "upper_quoted")
let countUpperQuoted = db2.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'UPPER_QUOTED'")
when usesLegacySqliteDropNames:
check countUpperQuoted == "1"
else:
check countUpperQuoted == "0"
db2.dropTable(staticSqlContent, "a\"b")
let countEscapedQuoted = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name = 'A\"B'"))
when usesLegacySqliteDropNames:
check countEscapedQuoted == "1"
else:
check countEscapedQuoted == "0"
db2.dropTable(staticSqlContent)
let countAll = db2.getValue(sql("select count(*) from sqlite_master where type='table' and name in ('lower_table','UPPER_TABLE','Quoted Table','Quoted Table2','UPPER_QUOTED','A\"B')"))
when usesLegacySqliteDropNames:
check countAll == "3"
else:
check countAll == "0"
+822
View File
@@ -0,0 +1,822 @@
import unittest, strformat, sequtils, algorithm, sugar, json, tables, random
import os
import ormin
import ormin/db_utils
when NimVersion < "1.2.0": import ./compat
let testDir = currentSourcePath.parentDir()
when defined postgre:
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
from db_connector/db_postgres import exec, getValue
const backend = DbBackend.postgre
importModel(backend, "forum_model_postgres")
const sqlFileName = "forum_model_postgres.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "forum_model_sqlite")
const sqlFileName = "forum_model_sqlite.sql"
var memoryPath = testDir & "/" & ":memory:"
let db {.global.} = open(memoryPath, "", "", "")
var sqlFilePath = Path(testDir & "/" & sqlFileName)
type
Person = tuple[id: int,
name: string,
password: string,
email: string,
salt: string,
status: string]
Thread = tuple[id: int,
name: string,
views: int]
Post = tuple[id: int,
author: int,
ip: string,
header: string,
content: string,
thread: int]
Antibot = tuple[id: int,
ip: string,
answer: string]
const
personcount = 5
threadcount = 5
postcount = 10
antibotcount = 5
var
persondata: seq[Person]
threaddata: seq[Thread]
postdata: seq[Post]
antibotdata: seq[Antibot]
suite &"Test ormin features of {backend}":
discard
suite "query":
db.dropTable(sqlFilePath)
db.createTable(sqlFilePath)
static:
doAssert compiles(block:
discard query:
select post(author)
crossjoin person(name)
)
doAssert compiles(block:
discard query:
select post(author)
outerjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
leftjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
leftouterjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
rightjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
rightouterjoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
fulljoin person(name) on author == id
)
doAssert compiles(block:
discard query:
select post(author)
fullouterjoin person(name) on author == id
)
# prepare data to insert
for i in 1..personcount:
persondata.add((id: i,
name: fmt"john{i}",
password: fmt"pass{i}",
email: fmt"john{i}@mail.com",
salt: fmt"abcd{i}",
status: fmt"ok{i}"))
for i in 1..threadcount:
threaddata.add((id: i,
name: fmt"thread{i}",
views: i))
for i in 1..postcount:
postdata.add((id: i,
author: sample({1..personcount}),
ip: "",
header: fmt"title{i}",
content: fmt"content{i}",
thread: sample({1..threadcount})))
for i in 1..antibotcount:
antibotdata.add((id: i,
ip: "",
answer: fmt"answer{i}"))
# insert data into database
let
insertperson = sql"insert into person (id, name, password, email, salt, status) values (?, ?, ?, ?, ?, ?)"
insertthread = sql"insert into thread (id, name, views) values (?, ?, ?)"
insertpost = sql"insert into post (id, author, ip, header, content, thread) values (?, ?, ?, ?, ?, ?)"
insertantibot = sql"insert into antibot (id, ip, answer) values (?, ?, ?)"
for p in persondata:
db.exec(insertperson, p.id, p.name, p.password, p.email, p.salt, p.status)
for t in threaddata:
db.exec(insertthread, t.id, t.name, t.views)
for p in postdata:
db.exec(insertpost, p.id, p.author, p.ip, p.header, p.content, p.thread)
for a in antibotdata:
db.exec(insertantibot, a.id, a.ip, a.answer)
# check data in database
let personexpected = db.getValue(sql"select count(*) from person")
assert personexpected == $personcount
let threadexpected = db.getValue(sql"select count(*) from thread")
assert threadexpected == $threadcount
let postexpected = db.getValue(sql"select count(*) from post")
assert postexpected == $postcount
let antibotexpected = db.getValue(sql"select count(*) from antibot")
assert antibotexpected == $antibotcount
test "table":
let res = query:
select person(id, name, password, email, salt, status)
check res == persondata
test "all_column":
let res = query:
select person(_)
check res.mapIt((it.id, it.name, it.password, it.email, it.salt, it.status)) == persondata
test "column_alias":
let res = query:
select person(id as personid, name as personname)
check res == persondata.mapIt((personid: it.id, personname: it.name))
test "arithmetic":
let res = query:
select person(id * 4 / 2 + 2 - 1 as id)
check res == persondata.mapIt(int(it.id * 4 / 2 + 2 - 1))
test "comparison":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
where id == ?id
check res == [persondata[id - 1]]
test "comparison_ne":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
where id != ?id
check res == persondata.filterIt(it.id != id)
test "comparison_ge":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id >= ?id
check res == persondata.filterIt(it.id >= id)
test "comparison_le":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id <= ?id
check res == persondata.filterIt(it.id <= id)
test "comparison_gt":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id > ?id
check res == persondata.filterIt(it.id > id)
test "comparison_lt":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where id < ?id
check res == persondata.filterIt(it.id < id)
test "logical_not":
let id = 3
let res = query:
select person(id, name, password, email, salt, status)
where not (id > ?id)
check res == persondata.filterIt(it.id <= id)
test "logical_and":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id >= ?id1 and id <= ?id2
check res == persondata.filterIt(it.id >= id1 and it.id <= id2)
test "logical_or":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id == ?id1 or id == ?id2
check res == persondata.filterIt(it.id == id1 or it.id == id2)
test "logical_complex":
let
id1 = 2
id2 = 3
id3 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id > ?id2 and (id == ?id1 or id == ?id3)
check res == persondata.filterIt(it.id == id3)
test "predicate_in":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id in ?id1 .. ?id2
check res == persondata.filterIt(it.id in id1..id2)
test "predicate_not_in":
let
id1 = 2
id2 = 4
let res = query:
select person(id, name, password, email, salt, status)
where id notin ?id1 .. ?id2
check res == persondata.filterIt(it.id < id1 or it.id > id2)
test "predicate_like":
let pattern = "john1%"
let res = query:
select person(id, name)
where name `like` ?pattern
check res == persondata.filterIt(it.name.startsWith("john1")).mapIt((it.id, it.name))
test "predicate_ilike":
let pattern = "JOHN1%"
let res = query:
select person(id, name)
where name `ilike` ?pattern
check res == persondata.filterIt(it.name.startsWith("john1")).mapIt((it.id, it.name))
test "predicate_not_like":
let pattern = "john1%"
let res = query:
select person(id, name)
where not (name `like` ?pattern)
check res == persondata.filterIt(not it.name.startsWith("john1")).mapIt((it.id, it.name))
test "distinct":
var res = query:
select `distinct` post(author)
res.sort()
let expected = postdata.mapIt(it.author).deduplicate().sortedByIt(it)
check res == expected
test "count_distinct":
let res = query:
select post(count(distinct author))
check res == @[postdata.mapIt(it.author).deduplicate().len]
test "window_row_number":
let res = query:
select post(author, id, over(row_number(), partitionby(author), orderby(id)) as rn)
orderby author, id
var expected: seq[(int, int, int)] = @[]
var counters = initCountTable[int]()
for p in postdata.sortedByIt((it.author, it.id)):
counters.inc(p.author)
expected.add((p.author, p.id, counters[p.author]))
check res == expected
test "window_running_sum":
let res = query:
select post(author, id, over(sum(id), partitionby(author), orderby(id)) as running)
orderby author, id
var expected: seq[(int, int, int)] = @[]
var sums = initTable[int, int]()
for p in postdata.sortedByIt((it.author, it.id)):
sums[p.author] = sums.getOrDefault(p.author) + p.id
expected.add((p.author, p.id, sums[p.author]))
check res == expected
test "limit":
let id = 1
let res = query:
select person(id, name, password, email, salt, status)
limit 1
static:
echo "LIMIT TEST: ", typeof(res)
check res == persondata[id - 1]
test "offset":
let res = query:
select person(id, name)
limit 2
offset 2
check res == persondata[2..3].mapIt((it.id, it.name))
test "match_assignment":
let id = 1
let (name, password, email, salt, status) = query:
select person(name, password, email, salt, status)
where id == ?id
limit 1
check:
name == persondata[id - 1].name
password == persondata[id - 1].password
email == persondata[id - 1].email
salt == persondata[id - 1].salt
status == persondata[id - 1].status
test "groupby":
let res = query:
select post(author, count(id))
groupby author
let counttable = postdata.mapIt(it.author).toCountTable()
for (author, c) in res:
check counttable[author] == c
test "groupby_aggregate":
let author = 3
let res = query:
select post(count(id))
where author == ?author
groupby author
let c = postdata.mapIt(it.author).toCountTable()[author]
check res == [c]
test "orderby":
let res = query:
select person(id, name)
orderby id
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_asc":
let res = query:
select person(id, name)
orderby asc(id)
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_desc":
let res = query:
select person(id, name)
orderby desc(id)
let expected = persondata.mapIt((it.id, it.name))
.sorted((x, y) => system.cmp(x[0], y[0]), Descending)
check res == expected
test "orderby_key_select":
let res = query:
select person(id as personid, name)
orderby personid
check res == persondata.mapIt((it.id, it.name)).sortedByIt(it[0])
test "orderby_key_not_select":
let res = query:
select person(name)
orderby id
check res == persondata.sortedByIt(it.id).mapIt(it.name)
test "orderby_mulitple":
# test fix #30 Incorrect handling of multiple sort keys in orderby
let res = query:
select post(author, id)
orderby author, desc(id)
check res == postdata.mapIt((it.author, it.id))
.sorted((x, y) => system.cmp(x[1], y[1]), Descending)
.sortedByIt(it[0])
test "having":
let id = 4
let res = query:
select post(author, count(_) as count)
groupby author
having author == ?id
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[0] == id: p
check res == expected
test "having_aggregate":
let countvalue = 2
let res = query:
select post(author, count(id) as count)
groupby author
having count(id) >= ?countvalue
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[1] >= countvalue: p
check res.sortedByIt(it[0]) == expected.sortedByIt(it[0])
test "having_complex":
let
authorid1 = 1
authorid2 = 3
countvalue = 2
let res = query:
select post(author, count(id) as count)
groupby author
having count(id) >= ?countvalue and (author == ?authorid1 or author == ?authorid2)
let expected = collect(newSeq):
for p in postdata.mapIt(it.author).toCountTable().pairs():
if p[0] in [authorid1, authorid2] and p[1] >= countvalue: p
check res == expected
test "subquery":
let res = query:
select post(id)
where author in
(select person(id))
check res.sortedByIt(it) == postdata.mapIt(it.id)
test "subquery_nest2":
let
personid1 = 1
personid2 = 2
expectedpost = postdata.filterIt(it.author in [personid1, personid2])
.mapIt(it.id)
let res = query:
select post(id)
where author in
(select person(id) where id == ?personid1 or id == ?personid2)
check res.sortedByIt(it) == expectedpost.sortedByIt(it)
test "subquery_nest3":
var res = query:
select thread(id)
where id in (select post(thread) where author in
(select person(id) where id in {1, 2}))
res.sort()
check res == postdata.filterIt(it.author in [1, 2])
.mapIt(it.thread)
.sortedByIt(it)
test "subquery_having":
# feature for having in subquery
let id = 4
let res = query:
select person(id)
where id in (
select post(author) groupby author having author == ?id)
check res == @[id]
test "exists":
let authorid = postdata[0].author
let res = query:
select person(id)
where exists(select post(id) where author == ?authorid)
check res.sortedByIt(it) == persondata.mapIt(it.id)
test "not_exists":
let missingAuthor = personcount + postcount
let res = query:
select person(id)
where not exists(select post(id) where author == ?missingAuthor)
check res.sortedByIt(it) == persondata.mapIt(it.id)
test "with_cte":
let res = query:
with recent(select post(id, author) where id <= 3)
select recent(author)
orderby id
check res == postdata.filterIt(it.id <= 3).sortedByIt(it.id).mapIt(it.author)
test "with_cte_chain":
let res = query:
with recent(select post(id, author) where id <= 3)
with authors(select recent(author))
select authors(author)
check res.sortedByIt(it) == postdata.filterIt(it.id <= 3).mapIt(it.author).sortedByIt(it)
test "with_cte_subquery":
let res = query:
with recent(select post(id, author) where id <= 3)
select person(id)
where id in (select recent(author))
check res.sortedByIt(it) == postdata.filterIt(it.id <= 3).mapIt(it.author).deduplicate().sortedByIt(it)
test "union":
var res = query:
select person(id) where id <= 2
union
select person(id) where id >= 4
res.sort()
check res == @[1, 2, 4, 5]
res = query:
union(
select person(id) where id <= 2,
select person(id) where id >= 4
)
res.sort()
check res == @[1, 2, 4, 5]
test "intersect":
var res = query:
select person(id) where id <= 3
intersect
select person(id) where id >= 2
res.sort()
check res == @[2, 3]
res = query:
intersect(
select person(id) where id <= 3,
select person(id) where id >= 2
)
res.sort()
check res == @[2, 3]
test "except":
var res = query:
select person(id) where id <= 4
`except`
select person(id) where id in {2, 3}
res.sort()
check res == @[1, 4]
res = query:
`except`(
select person(id) where id <= 4,
select person(id) where id in {2, 3}
)
res.sort()
check res == @[1, 4]
test "complex_query_subquery_having":
let
id1 = 2
id2 = 3
let res = query:
select thread(count(_))
where id in (
select post(thread) where (author == ?id1 or author == ?id2) and id in (
select post(min(id)) groupby thread having min(id) > 3))
limit 1
let threadinpost = postdata.mapIt(it.thread).deduplicate().sortedByIt(it)
var postminids: seq[int]
for id in threadinpost:
let group = collect(newSeq):
for p in postdata:
if p.thread == id: p.id
postminids.add(group.min())
let threadids = postdata.filterIt(it.author in [id1, id2] and
it.id in postminids.filterIt(it > 3))
.mapIt(it.thread)
check res == threadids.len()
test "auto_join":
let postid = 1
let res = query:
select post(author)
join person(name)
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "join_on":
# test fix #29 Cannot handle join on condition correctly
let postid = 1
let res = query:
select post(author)
join person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "leftjoin_on":
let postid = 1
let res = query:
select post(author)
leftjoin person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "leftouterjoin_on":
let postid = 1
let res = query:
select post(author)
leftouterjoin person(name) on author == id
where id == ?postid
let (author, name) = res[0]
check name == persondata[author - 1].name
test "casewhen":
# need more test, only number
# has problem with string or expression in condition
let res = query:
select person(name, (if id == 1: 10 elif id == 2: 20 else: 30) as pid)
check res == persondata.mapIt((
name: it.name,
pid: if it.id == 1: 10 elif it.id == 2: 20 else: 30))
test "produce_json":
# test fix #27 Error: type mismatch: got <typeof(nil)>
let threadjson = query:
select thread(id, name)
produce json
let expected = threaddata.map do (it: tuple) -> JsonNode:
result = newJObject()
result["id"] = %it.id
result["name"] = %it.name
check threadjson == %expected
test "produce_nim":
let threadnim = query:
select thread(id, name)
produce nim
check threadnim == threaddata.mapIt((it.id, it.name))
test "tryquery":
# unknow use, only not raise dbError?
let res = tryQuery:
select thread(id)
check res == threaddata.mapIt(it.id)
test "createproc":
createProc getAllThreadIds:
select thread(id)
check db.getAllThreadIds() == threaddata.mapIt(it.id)
test "createiter":
createIter allThreadIdsIter:
select thread(id)
let res = collect(newSeq):
for id in db.allThreadIdsIter:
id
check res.sortedByIt(it) == threaddata.mapIt(it.id)
test "update_concat_op":
let
id = 3
name = persondata[id - 1].name
appendstr = "updated"
nameupdated = name & appendstr
query:
update person(name = name & ?appendstr)
where id == ?id
let res = query:
select person(name)
where id == ?id
check res[0] == nameupdated
test "delete_one":
# test fix #14: delete not return value
let id = 1
query:
delete antibot
where id == ?id
let res = query:
select antibot(_)
where id == ?id
check res == []
test "delete_all":
query:
delete antibot
let res = query:
select antibot(_)
check res == []
test "insert_return_id":
# test fix #28 returning id fail under postgresql
let expectedid = 6
let id = query:
insert antibot(id = ?expectedid, ip = "", answer = "just insert")
returning id
check id == expectedid
test "insert_return_answer":
# test returning non-id parameter
let expectedanswer = "just insert another"
let answer = query:
insert antibot(id = 9, ip = "", answer = ?expectedanswer)
returning answer
check answer == expectedanswer
test "insert_return_id_auto":
# test returning id column
when defined(postgre):
# fix postgres sequence so next nextval returns 10
discard db.getValue(sql"select setval('antibot_id_seq', ?, ?)", 10, false)
let answer = query:
insert antibot(ip = "", answer = "just auto insert")
returning id
check answer == 10
test "insert_returning_uuid":
# test returning uuid column
let expecteduuid = "123e4567-e89b-12d3-a456-426614174000"
let uuid = query:
insert error(uuid = ?expecteduuid, message = "just insert")
returning uuid
check uuid == expecteduuid
test "where_json":
let
id = 1
p = %*{"id": id}
let res = query:
select person(id)
where id == %p["id"]
check res == [id]
test "insert_json":
let
id = 7
answer = "json answer"
a = %*{"answer": answer}
query:
insert antibot(id = ?id, ip = "", answer = %a["answer"])
let res = query:
select antibot(answer)
where id == ?id
check res[0] == answer
test "update_json":
let
id = 3
name = "json"
p = %*{"name": name}
query:
update person(name = %p["name"])
where id == ?id
let res = query:
select person(name)
where id == ?id
check res[0] == name
test "insert_rawsql":
let id = 8
query:
insert antibot(id = ?id, ip = "", answer = !!"'raw sql'",
created = !!"CURRENT_TIMESTAMP")
let res = query:
select antibot(_)
where id == ?id
check res[0].answer == "raw sql"
test "update_rawsql":
let id = 3
let res = query:
select person(name)
where id == ?id
query:
update person(name = !!"UPPER(name)")
where id == ?id
let res2 = query:
select person(name)
where id == ?id
check res[0] != res2[0]
check res[0].toUpper() == res2[0]
test "empty_string":
db.exec(insertthread, 6, "", 77)
createProc getAllThreads:
select thread(id, name, views)
where id >= 5
let rows = db.getAllThreads
check rows[1].id == 6
check rows[1].views == 77
check rows[1].name == ""
+29
View File
@@ -0,0 +1,29 @@
import unittest
import ormin/ormin_sqlite as orm_sqlite
from db_connector/db_sqlite import exec, getValue
import ormin
importModel(DbBackend.sqlite, "static_only_model", includeStatic = true)
let db {.global.} = orm_sqlite.open(":memory:", "", "", "")
suite "importModel includeStatic":
test "sqlSchema is generated and usable without a generated Nim model":
let schema: DbSql = sqlSchema
check ($schema).len > 0
db.createTable(sqlSchema)
db.exec(sql"insert into user_static (id, name) values (?, ?)", 1, "Ada")
let row = query:
select user_static(id, name)
where id == 1
limit 1
check row.id == 1
check row.name == "Ada"
check db.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'user_static'") == "1"
db.dropTable(sqlSchema, "user_static")
check db.getValue(sql"select count(*) from sqlite_master where type='table' and name = 'user_static'") == "0"
+130
View File
@@ -0,0 +1,130 @@
import unittest, json, strutils, macros, times, os, sequtils
# Postgres connection handled through ormin_postgre backend
from db_connector/db_postgres import exec, getValue
import ormin
import ormin/ormin_postgre as ormin_postgre
# import db_connector/db_postgres as db_postgres
import ormin/db_utils
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
importModel(DbBackend.postgre, "model_postgre")
let
db {.global.} = ormin_postgre.open("localhost", "test", "test", "test_ormin")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "model_postgre.sql")
suite "Test special database types and functions of postgre":
discard
let
dtStr1 = "2018-03-30 01:01:01"
dt1 = parse(dtStr1, "yyyy-MM-dd HH:mm:ss")
dtnStr1 = "2019-03-30 11:02:02+08"
dtn1 = parse(dtnStr1, "yyyy-MM-dd HH:mm:sszz")
dtzStr1 = "2020-03-30 09:03:03Z"
dtz1 = parse(dtzStr1, "yyyy-MM-dd HH:mm:sszz")
dtStr2 = "2018-03-30 01:01:01.123"
dt2 = parse(dtStr2, "yyyy-MM-dd HH:mm:ss\'.\'fff")
dtnStr2 = "2019-03-30 11:02:02.123+08"
dtn2 = parse(dtnStr2, "yyyy-MM-dd HH:mm:ss\'.\'fffzz")
dtzStr2 = "2020-03-30 09:03:03.123Z"
dtz2 = parse(dtzStr2, "yyyy-MM-dd HH:mm:ss\'.\'fffzz")
dtStr3 = "2018-03-30 01:01:01.123456"
dt3 = parse(dtStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffff")
dtnStr3 = "2019-03-30 11:02:02.123456+08"
dtn3 = parse(dtnStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
dtzStr3 = "2020-03-30 09:03:03.123456Z"
dtz3 = parse(dtzStr3, "yyyy-MM-dd HH:mm:ss\'.\'ffffffzz")
dtjson1 = %*{"dt": dt1.format(jsonTimeFormat),
"dtn": dtn1.format(jsonTimeFormat),
"dtz": dtz1.format(jsonTimeFormat)}
dtjson2 = %*{"dt": dt2.format(jsonTimeFormat),
"dtn": dtn2.format(jsonTimeFormat),
"dtz": dtz2.format(jsonTimeFormat)}
dtjson3 = %*{"dt": dt3.format(jsonTimeFormat),
"dtn": dtn3.format(jsonTimeFormat),
"dtz": dtz3.format(jsonTimeFormat)}
let insertSql = sql"insert into tb_timestamp(dt, dtn, dtz) values (?, ?, ?)"
suite "timestamp_insert":
setup:
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
test "insert":
query:
insert tb_timestamp(dt = ?dt1, dtn = ?dtn1, dtz = ?dtz1)
check db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "1"
test "json":
query:
insert tb_timestamp(dt = %dtjson1["dt"],
dtn = %dtjson1["dtn"],
dtz = %dtjson1["dtz"])
check db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "1"
suite "timestamp":
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
db_postgres.exec(db, insertSql, dtStr1, dtnStr1, dtzStr1)
db_postgres.exec(db, insertSql, dtStr2, dtnStr2, dtzStr2)
db_postgres.exec(db, insertSql, dtStr3, dtnStr3, dtzStr3)
doAssert db_postgres.getValue(db, sql"select count(*) from tb_timestamp") == "3"
test "query":
let res = query:
select tb_timestamp(dt, dtn, dtz)
check res == [(dt1, dtn1, dtz1),
(dt2, dtn2, dtz2),
(dt3, dtn3, dtz3)]
test "where":
let res = query:
select tb_timestamp(dt)
where dt == ?dt1
check res == [dt1]
test "in":
let
duration = initDuration(hours = 1)
dtStart = dt1 - duration
dtEnd = dt1 + duration
res2 = query:
select tb_timestamp(dt)
where dt in ?dtStart .. ?dtEnd
check res2 == [dt1, dt2, dt3]
test "iter":
createIter iter:
select tb_timestamp(dt)
where dt == ?dt
var res: seq[DateTime]
for it in db.iter(dt1):
res.add(it)
check res == [dt1]
test "proc":
createProc aproc:
select tb_timestamp(dt)
where dt == ?dt
check db.aproc(dt1) == [dt1]
test "json":
let res = query:
select tb_timestamp(dt, dtn, dtz)
produce json
check res == %*[dtjson1, dtjson2, dtjson3]
test "json_where":
let res = query:
select tb_timestamp(dt, dtn, dtz)
where dt == %dtjson1["dt"]
produce json
check res[0] == dtjson1
+216
View File
@@ -0,0 +1,216 @@
import unittest, strformat, os, times, std/monotimes
import std/options
import ormin
import ormin/db_utils
import ormin/query_hooks
when defined(postgre):
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
const backend = DbBackend.postgre
importModel(backend, "model_postgre")
const sqlFileName = "model_postgre.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
const backend = DbBackend.sqlite
importModel(backend, "model_sqlite")
const sqlFileName = "model_sqlite.sql"
let db {.global.} = open("test.db", "", "", "")
let
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / sqlFileName)
type
CompositeRow = object
id: int
message: string
RefCompositeRow = ref object
id: int
message: string
BenchmarkCompositeRow = object
pk1: int
message: string
NullableNoteOptionRow = object
id: int
note: Option[string]
MessageSize = distinct int
HookedMessageRow = object
id: int
message: MessageSize
NullFallbackNote = distinct string
HookedNullableNoteRow = object
id: int
note: NullFallbackNote
const
benchmarkRowCount = 256
benchmarkWarmupIterations = 75
benchmarkIterations = 250
benchmarkRounds = 5
maxTypedQuerySlowdown = 1.20
proc fromQueryHook*(val: var MessageSize, value: string) =
val = MessageSize(value.len)
proc fromQueryHook*(val: var NullFallbackNote, value: DbValue[string]) =
if value.isNull:
val = NullFallbackNote("<missing>")
else:
val = NullFallbackNote("note:" & value.value)
proc loadBenchmarkRows() =
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
for i in 1 .. benchmarkRowCount:
let message = &"message-{i}"
query:
insert tb_composite_pk(pk1 = ?i, pk2 = ?i, message = ?message)
proc benchmarkCurrentQuery(iterations: int): float =
var checksum = 0
let started = getMonoTime()
for _ in 0 ..< iterations:
let rows = query:
select tb_composite_pk(pk1, message)
orderby pk1
checksum += rows.len + rows[^1][0] + rows[^1][1].len
doAssert checksum > 0
result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0
proc benchmarkTypedQuery(iterations: int): float =
var checksum = 0
let started = getMonoTime()
for _ in 0 ..< iterations:
let rows = query(BenchmarkCompositeRow):
select tb_composite_pk(pk1, message)
orderby pk1
checksum += rows.len + rows[^1].pk1 + rows[^1].message.len
doAssert checksum > 0
result = (getMonoTime() - started).inNanoseconds.float / 1_000_000_000.0
suite &"query(T) mapping on {backend}":
setup:
db.dropTable(sqlFile, "tb_composite_pk")
db.createTable(sqlFile, "tb_composite_pk")
db.dropTable(sqlFile, "tb_nullable")
db.createTable(sqlFile, "tb_nullable")
query:
insert tb_composite_pk(pk1 = 1, pk2 = 1, message = "hello")
query:
insert tb_composite_pk(pk1 = 2, pk2 = 2, message = "world")
query:
insert tb_nullable(id = 1, note = nil)
query:
insert tb_nullable(id = 2, note = "hello")
test "maps selected rows to objects":
let rows = query(CompositeRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows == @[
CompositeRow(id: 1, message: "hello"),
CompositeRow(id: 2, message: "world")
]
test "maps selected rows to ref objects":
let rows = query(RefCompositeRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows.len == 2
check rows[0] != nil
check rows[0].id == 1
check rows[0].message == "hello"
check rows[1] != nil
check rows[1].id == 2
check rows[1].message == "world"
test "single-row query(T) returns a single object":
let row = query(CompositeRow):
select tb_composite_pk(pk1 as id, message)
where pk1 == 1 and pk2 == 1
limit 1
check row == CompositeRow(id: 1, message: "hello")
test "maps nullable column to Option":
let rows = query(NullableNoteOptionRow):
select tb_nullable(id, note)
orderby id
check rows[0].id == 1
check rows[0].note.isNone
check rows[1].id == 2
check rows[1].note.isSome
check rows[1].note.get == "hello"
test "maps object fields through user fromQueryHook overloads":
let rows = query(HookedMessageRow):
select tb_composite_pk(pk1 as id, message)
orderby pk1
check rows.len == 2
check rows[0].id == 1
check int(rows[0].message) == "hello".len
check rows[1].id == 2
check int(rows[1].message) == "world".len
test "maps scalar query(T) through user fromQueryHook overloads":
let values = query(MessageSize):
select tb_composite_pk(message)
orderby pk1
check values.len == 2
check int(values[0]) == "hello".len
check int(values[1]) == "world".len
test "allows user fromQueryHook overloads to handle NULL values":
let rows = query(HookedNullableNoteRow):
select tb_nullable(id, note)
orderby id
check rows.len == 2
check rows[0].id == 1
check string(rows[0].note) == "<missing>"
check rows[1].id == 2
check string(rows[1].note) == "note:hello"
test "sqlite benchmark for query and query(T)":
loadBenchmarkRows()
let untypedRows = query:
select tb_composite_pk(pk1, message)
orderby pk1
let typedRows = query(BenchmarkCompositeRow):
select tb_composite_pk(pk1, message)
orderby pk1
check untypedRows.len == typedRows.len
check typedRows[0] == BenchmarkCompositeRow(pk1: untypedRows[0][0], message: untypedRows[0][1])
check typedRows[^1] == BenchmarkCompositeRow(pk1: untypedRows[^1][0], message: untypedRows[^1][1])
discard benchmarkCurrentQuery(benchmarkWarmupIterations)
discard benchmarkTypedQuery(benchmarkWarmupIterations)
var currentBest = high(float)
var typedBest = high(float)
for _ in 0 ..< benchmarkRounds:
currentBest = min(currentBest, benchmarkCurrentQuery(benchmarkIterations))
typedBest = min(typedBest, benchmarkTypedQuery(benchmarkIterations))
let ratio = typedBest / currentBest
echo &"sqlite benchmark query={currentBest:.6f}s query(T)={typedBest:.6f}s ratio={ratio:.3f}x; 20% budget={(ratio <= maxTypedQuerySlowdown)}"
check currentBest > 0.0
check typedBest > 0.0
when defined(release):
check ratio <= maxTypedQuerySlowdown
+153
View File
@@ -0,0 +1,153 @@
import unittest, db_connector/sqlite3, json, times, sequtils
from db_connector/db_sqlite import exec, getValue
import ormin
import ormin/db_utils
importModel(DbBackend.sqlite, "model_sqlite")
let
db {.global.} = open(":memory:", "", "", "")
testDir = currentSourcePath.parentDir()
sqlFile = Path(testDir / "model_sqlite.sql")
suite "Test special database types and functions of sqlite":
discard
jsonTimeFormat = "yyyy-MM-dd HH:mm:ss\'.\'fff"
let
dtStr1 = "2018-02-20 02:02:02"
dt1 = parse(dtStr1, "yyyy-MM-dd HH:mm:ss", utc())
dtStr2 = "2019-03-30 03:03:03.123"
dt2 = parse(dtStr2, "yyyy-MM-dd HH:mm:ss\'.\'fff", utc())
dtjson = %*{"dt1": dt1.format(jsonTimeFormat),
"dt2": dt2.format(jsonTimeFormat)}
let insertSql = sql"insert into tb_timestamp(dt1, dt2) values (?, ?)"
proc blobFromBytes(bytes: openArray[int]): seq[byte] =
result = newSeq[byte](bytes.len)
for i, b in bytes:
doAssert b >= 0 and b <= 255
result[i] = byte(b)
let blobFixtures = [
blobFromBytes(@[0, 1, 2, 3, 4, 5, 6, 7]),
blobFromBytes(@[255, 128, 64, 32, 16, 8, 4, 2, 1]),
blobFromBytes(@[ord('O'), ord('R'), ord('M'), ord('I'), ord('N'), 0, 1, 2])
]
suite "timestamp_insert":
setup:
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
test "insert":
query:
insert tb_timestamp(dt1 = ?dt1, dt2 = ?dt2)
check db.getValue(sql"select count(*) from tb_timestamp") == "1"
test "json":
query:
insert tb_timestamp(dt1 = %dtjson["dt1"], dt2 = %dtjson["dt2"])
check db.getValue(sql"select count(*) from tb_timestamp") == "1"
suite "timestamp":
db.dropTable(sqlFile, "tb_timestamp")
db.createTable(sqlFile, "tb_timestamp")
db.exec(insertSql, dtStr1, dtStr2)
doAssert db.getValue(sql"select count(*) from tb_timestamp") == "1"
test "query":
let res = query:
select tb_timestamp(dt1, dt2)
check res == [(dt1, dt2)]
test "where":
let res = query:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
check res == [(dt1, dt2)]
test "in":
let
duration = initDuration(hours = 1)
dtStart = dt1 - duration
dtEnd = dt1 + duration
res2 = query:
select tb_timestamp(dt1, dt2)
where dt1 in ?dtStart .. ?dtEnd
check res2 == [(dt1, dt2)]
test "iter":
createIter iter:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
var res: seq[tuple[dt1: DateTime, dt2: DateTime]]
for it in db.iter(dt1):
res.add(it)
check res == [(dt1, dt2)]
test "proc":
createProc aproc:
select tb_timestamp(dt1, dt2)
where dt1 == ?dt1
check db.aproc(dt1) == [(dt1, dt2)]
test "json":
let res = query:
select tb_timestamp(dt1, dt2)
produce json
check res == %*[dtjson]
test "json_where":
let res = query:
select tb_timestamp(dt1, dt2)
where dt1 == %dtjson["dt1"]
produce json
check res == %*[dtjson]
suite "blob_insert":
setup:
db.dropTable(sqlFile, "tb_blob")
db.createTable(sqlFile, "tb_blob")
test "insert parameters":
for blob in blobFixtures:
query:
insert tb_blob(typblob = ?blob)
check db.getValue(sql"select count(*) from tb_blob") == $blobFixtures.len
suite "blob":
db.dropTable(sqlFile, "tb_blob")
db.createTable(sqlFile, "tb_blob")
for blob in blobFixtures:
query:
insert tb_blob(typblob = ?blob)
doAssert db.getValue(sql"select count(*) from tb_blob") == $blobFixtures.len
test "query":
let res = query:
select tb_blob(id, typblob)
check res.mapIt(it.typblob) == blobFixtures
test "where":
let target = blobFixtures[1]
let res = query:
select tb_blob(id, typblob)
where typblob == ?target
check res.len == 1
check res[0].typblob == target
createProc selectAllBlob:
select tb_blob(id, typblob)
test "createProc":
let res = db.selectAllBlob
check res.mapIt(it.typblob) == blobFixtures
test "createProc with empty blob in result":
db.exec(sql"insert into tb_blob(id, typblob) values (?, ?)", 9, "")
let res = db.selectAllBlob
check res[^1].typblob == blobFromBytes(@[])
+132
View File
@@ -0,0 +1,132 @@
import unittest, os, strformat
import ormin
import ormin/db_utils
when NimVersion < "1.2.0": import ./compat
let testDir = currentSourcePath.parentDir()
when defined postgre:
when defined(macosx):
{.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
from db_connector/db_postgres import exec, getValue
const backend = DbBackend.postgre
importModel(backend, "forum_model_postgres")
const sqlFileName = "forum_model_postgres.sql"
let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
from db_connector/db_sqlite import exec, getValue
const backend = DbBackend.sqlite
importModel(backend, "forum_model_sqlite")
const sqlFileName = "forum_model_sqlite.sql"
var memoryPath = testDir & "/" & ":memory:"
let db {.global.} = open(memoryPath, "", "", "")
var sqlFilePath = Path(testDir & "/" & sqlFileName)
# Fresh schema
db.dropTable(sqlFilePath)
db.createTable(sqlFilePath)
suite &"Transactions ({backend})":
test "commit on success":
transaction:
query:
insert person(id = ?(101), name = ?"john101", password = ?"p101", email = ?"john101@mail.com", salt = ?"s101", status = ?"ok")
check db.getValue(sql"select count(*) from person where id = 101") == "1"
test "rollback on error with manual try except":
# prepare one row
query:
insert person(id = ?(201), name = ?"john201", password = ?"p201", email = ?"john201@mail.com", salt = ?"s201", status = ?"ok")
# in transaction insert a new row and then violate PK
try:
transaction:
query:
insert person(id = ?(202), name = ?"john202", password = ?"p202", email = ?"john202@mail.com", salt = ?"s202", status = ?"ok")
# duplicate key error
query:
insert person(id = ?(201), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
check false # should not reach
except DbError as e:
discard
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 202") == "0"
check db.getValue(sql"select count(*) from person where id = 201 and name = 'dup'") == "0"
test "rollback on error with else":
# prepare one row
var failed = false
query:
insert person(id = ?(501), name = ?"john501", password = ?"p501", email = ?"john501@mail.com", salt = ?"s501", status = ?"ok")
# in transaction insert a new row and then violate PK
transaction:
query:
insert person(id = ?(502), name = ?"john502", password = ?"p502", email = ?"john502@mail.com", salt = ?"s502", status = ?"ok")
# duplicate key error
query:
insert person(id = ?(501), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
check false # should not reach
else:
echo "do something else..."
failed = true
check failed
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 502") == "0"
check db.getValue(sql"select count(*) from person where id = 501 and name = 'dup'") == "0"
test "commit normally with else":
# prepare one row
var failed = false
query:
insert person(id = ?(601), name = ?"john601", password = ?"p601", email = ?"john601@mail.com", salt = ?"s601", status = ?"ok")
# in transaction insert a new row and then violate PK
transaction:
query:
insert person(id = ?(602), name = ?"john602", password = ?"p602", email = ?"john602@mail.com", salt = ?"s602", status = ?"ok")
query:
insert person(id = ?(603), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
failed = true
check not failed
# both inserts inside the transaction should be rolled back
check db.getValue(sql"select count(*) from person where id = 603") == "1"
check db.getValue(sql"select count(*) from person where id = 602") == "1"
check db.getValue(sql"select count(*) from person where id = 601") == "1"
test "transaction set false on DbError":
var failed = false
transaction:
query:
insert person(id = ?(301), name = ?"john301", password = ?"p301", email = ?"john301@mail.com", salt = ?"s301", status = ?"ok")
query:
insert person(id = ?(301), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
failed = true
check failed
check db.getValue(sql"select count(*) from person where id = 301") == "0"
test "nested savepoints":
var failed = false
transaction:
query:
insert person(id = ?(401), name = ?"john401", password = ?"p401", email = ?"john401@mail.com", salt = ?"s401", status = ?"ok")
var innerOk = true
transaction:
query:
insert person(id = ?(402), name = ?"john402", password = ?"p402", email = ?"john402@mail.com", salt = ?"s402", status = ?"ok")
query:
insert person(id = ?(401), name = ?"dup401", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
else:
innerOk = false
check innerOk == false
# after inner rollback, we can still insert another row and commit outer
query:
insert person(id = ?(403), name = ?"john403", password = ?"p403", email = ?"john403@mail.com", salt = ?"s403", status = ?"ok")
else:
failed = true
check db.getValue(sql"select count(*) from person where id in (401,402,403)") == "2"