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
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:
@@ -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)
|
||||
@@ -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);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
);
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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 == ""
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(@[])
|
||||
@@ -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"
|
||||
Reference in New Issue
Block a user