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 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 == ""