From 47debb8d994158989496ce3b7d81d41e3db27936 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Sat, 15 Apr 2023 21:58:26 +0200 Subject: [PATCH 01/27] init --- .gitignore | 3 +- README.md | 346 ++++++++++---------- sqlbuilder.nimble | 2 +- src/sqlbuilder.nim | 395 ++--------------------- src/sqlbuilderpkg/delete.nim | 10 + src/sqlbuilderpkg/insert.nim | 9 + src/sqlbuilderpkg/select.nim | 392 +++++++++++++++++++++- src/sqlbuilderpkg/select_legacy.nim | 232 +++++++++++++ src/sqlbuilderpkg/update.nim | 11 + src/sqlbuilderpkg/utils.nim | 164 ++++++++++ tests/test.nim | 482 +++++++++++++++++++++++++++- tests/test2.nim | 118 +++++++ 12 files changed, 1615 insertions(+), 549 deletions(-) create mode 100644 src/sqlbuilderpkg/select_legacy.nim create mode 100644 src/sqlbuilderpkg/utils.nim create mode 100644 tests/test2.nim diff --git a/.gitignore b/.gitignore index 9c80082..c970c19 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ tests/test -src/sqlbuilder \ No newline at end of file +src/sqlbuilder +.vscode \ No newline at end of file diff --git a/README.md b/README.md index 5e33eb7..64e1496 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,63 @@ SQL builder for ``INSERT``, ``UPDATE``, ``SELECT`` and ``DELETE`` queries. The builder will check for NULL values and build a query with them. -After Nim's update to 0.19.0, the check for NULL values has been removed +After Nim's update to 0.19.0, the check for NULL values was removed due to the removal of ``nil``. -This library's main goal is to allow the user, to insert NULL values into the -database again, and ease the creating of queries. +This library allows the user, to insert NULL values into queries and +ease the creating of queries. + + + +# Importing + +## Import all +```nim +import sqlbuilder +``` + +## Import only the SELECT builder +```nim +import sqlbuilder/select +``` + +## Import all but with legacy softdelete fix +```nim +# nano sqlfile.nim +# import this file instead of sqlbuilder + +import src/sqlbuilderpkg/insert +export insert + +import src/sqlbuilderpkg/update +export update + +import src/sqlbuilderpkg/delete +export delete + +import src/sqlbuilderpkg/utils +export utils + +# This enables the softdelete columns for the legacy selector +const tablesWithDeleteMarker = ["tasks", "persons"] +# Notice the include instead of import +include src/sqlbuilderpkg/select +``` + # Macro generated queries - -The library supports generating the queries with a macro, which improves the -performance due to query being generated on compile time. The macro generated + +The library supports generating some queries with a macro, which improves the +performance due to query being generated on compile time. + +The macro generated queries **do not** accept the `genArgs()` and `genArgsColumns()` due to not being available on compile time - so there's currently not NULL-support for macros. + # NULL values @@ -196,6 +237,7 @@ the column does not exists, it will be skipped in the query. + # Examples (INSERT) ## Insert without NULL @@ -224,183 +266,147 @@ the column does not exists, it will be skipped in the query. Please note that you have to define the equal symbol for the where clause. This allows you to use `=`, `!=`, `>`, `<`, etc. -## Select without NULL - +## Select query builder + +The SELECT builder gives access to the following fields: + +* BASE + * table: string, + * select: varargs[string], + * where: varargs[string], +* JOIN + * joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], + * jointype: SQLJoinType = LEFT, +* WHERE IN + * whereInField: string = "", + * whereInValue: seq[string] = @[], + * whereInValueString: seq[string] = @[], + * whereInValueInt: seq[int] = @[], +* Custom SQL, e.g. ORDER BY + * customSQL: string = "", +* Null checks + * checkedArgs: ArgsContainer.query = @[], +* Table alias + * tableAs: string = table, +* Soft delete + * hideIsDeleted: bool = true, + * tablesWithDeleteMarker: varargs[string] = @[], + * deleteMarker = ".is_deleted IS NULL", + +## Example on builder ```nim - getValue(db, sqlSelect("myTable", - ["email", "age"], [""], ["name ="], "", "", ""), "John") - # SELECT email, age FROM myTable WHERE name = ? - - getValue(db, sqlSelect("myTable", - ["myTable.email", "myTable.age", "company.name"], - ["company ON company.email = myTable.email"], - ["myTable.name =", "myTable.age ="], "", "", ""), - "John", "20") - # SELECT myTable.email, myTable.age, company.name - # FROM myTable - # LEFT JOIN company ON company.email = myTable.email - # WHERE myTable.name = ? AND myTable.age = ? - - getAllRows(db, sqlSelect("myTable", - ["myTable.email", "myTable.age", "company.name"], - ["company ON company.email = myTable.email"], - ["company.name ="], "20,22,24", "myTable.age", "ORDER BY myTable.email"), - "BigBiz") - # SELECT myTable.email, myTable.age, company.name - # FROM myTable LEFT JOIN company ON company.email = myTable.email - # WHERE company.name = ? AND myTable.age IN (20,22,24) - # ORDER BY myTable.email +test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + jointype = INNER +) +check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) ``` - -## Select with NULL - ```nim - let a = genArgs(dbNullVal) - getValue(db, sqlSelect("myTable", - ["email", "age"], [""], ["name ="], "", "", "", a.query), a.args) - # SELECT email, age FROM myTable WHERE name = NULL - - let a = genArgs("John", dbNullVal) - getValue(db, sqlSelect("myTable", - ["myTable.email", "myTable.age", "company.name"], - ["company ON company.email = myTable.email"], - ["myTable.name =", "myTable.age ="], "", "", "", a.query), a.args) - # SELECT myTable.email, myTable.age, company.name - # FROM myTable - # LEFT JOIN company ON company.email = myTable.email - # WHERE myTable.name = ? AND myTable.age = NULL - - let a = genArgs(dbNullVal) - getAllRows(db, sqlSelect("myTable", - ["myTable.email", "myTable.age", "company.name"], - ["company ON company.email = myTable.email"], - ["company.name ="], "20,22,24", "myTable.age", "ORDER BY myTable.email", a.query), - a.args) - # SELECT myTable.email, myTable.age, company.name - # FROM myTable LEFT JOIN company ON company.email = myTable.email - # WHERE company.name = NULL AND myTable.age IN (20,22,24) - # ORDER BY myTable.email +test = sqlSelect( + table = "tasksitems", + tableAs = "tasks", + select = @[ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + where = @[ + "projects.id =", + "tasks.status >" + ], + joinargs = @[ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) + ], + whereInField = "tasks.id", + whereInValue = @["1", "2", "3"], + customSQL = "ORDER BY tasks.created DESC", + tablesWithDeleteMarker = tableWithDeleteMarker +) +check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1 AND his.is_deleted IS NULL) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + AND tasks.is_deleted IS NULL + ORDER BY + tasks.created DESC + """))) ``` -# Debug -If you need to debug and see the queries, just pass `-d:testSqlquery` to print -the queries. -# Credit -Inspiration for builder: [Nim Forum](https://github.com/nim-lang/nimforum) +## Convert legacy -# Imports -import strutils, db_common +The legacy SELECT builder is deprecated and will be removed in the future. It +is commented out in the source code, and a converter has been added to convert +the legacy query to the new builder. -# Types -## Procs -### proc argType* -```nim -proc argType*(v: ArgObj): ArgObj = -``` -Checks if a ``ArgObj`` is NULL and return -``dbNullVal``. If it's not NULL, the passed -``ArgObj`` is returned. -### proc argType* -```nim -proc argType*(v: string | int): ArgObj = -``` -Transforms a string or int to a ``ArgObj`` -### proc dbValOrNull* -```nim -proc dbValOrNull*(v: string | int): ArgObj = -``` -Return NULL obj if len() == 0, else return value obj -### proc sqlInsert* -```nim -proc sqlInsert*(table: string, data: varargs[string], args: ArgsContainer.query): SqlQuery = -``` -SQL builder for INSERT queries -Checks for NULL values -### proc sqlInsert* -```nim -proc sqlInsert*(table: string, data: varargs[string]): SqlQuery = -``` -SQL builder for INSERT queries -Does NOT check for NULL values -### proc sqlUpdate* -```nim -proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], args: ArgsContainer.query): SqlQuery = -``` -SQL builder for UPDATE queries -Checks for NULL values -### proc sqlUpdate* -```nim -proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string]): SqlQuery = -``` -SQL builder for UPDATE queries -Does NOT check for NULL values -### proc sqlDelete* -```nim -proc sqlDelete*(table: string, where: varargs[string]): SqlQuery = -``` -SQL builder for DELETE queries -Does NOT check for NULL values -### proc sqlDelete* -```nim -proc sqlDelete*(table: string, where: varargs[string], args: ArgsContainer.query): SqlQuery = -``` -SQL builder for DELETE queries -Checks for NULL values -### proc sqlSelect* -```nim -proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = -``` -SQL builder for SELECT queries -Does NOT check for NULL values -### proc sqlSelect* -```nim -proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, args: ArgsContainer.query): SqlQuery = -``` -SQL builder for SELECT queries -Checks for NULL values -## Templates -### template genArgs*[T] -```nim -template genArgs*[T](arguments: varargs[T, argType]): ArgsContainer = -``` -Create argument container for query and passed args +That means, you don't have to worry, but you should definitely convert your +legacy queries to the new builder. -### template genArgsColumns*[T] ```nim -template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq[string], args: ArgsContainer] = +# Legacy builder +test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "", "", "") + +check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + """)) ``` -Create argument container for query and passed args and selecting which -columns to update. It's and expansion of `genArgs()`, since you can -decide which columns, there should be included. -Lets say, that you are importing data from a spreadsheet with static -columns, and you need to update your DB with the new values. -If the spreadsheet contains an empty field, you can update your DB with the -NULL value. But sometimes the spreadsheet only contains 5 out of the 10 -static columns. Now you don't know the values of the last 5 columns, -which will result in updating the DB with NULL values. -This template allows you to use the same query, but dynamic selecting only the -columns which shall be updated. When importing your spreadsheet, check -if the column exists (bool), and pass that as the `use: bool` param. If -the column does not exists, it will be skipped in the query. +# Examples -## Other -### ArgObj* -```nim -ArgObj* = object -``` -Argument object -### ArgsContainer -```nim -ArgsContainer = object -``` -Argument container used for queries and args -### var dbNullVal* -```nim -var dbNullVal*: ArgObj -``` -Global NULL value +See the test files. + + + +# Credit +Inspiration for builder: [Nim Forum](https://github.com/nim-lang/nimforum) diff --git a/sqlbuilder.nimble b/sqlbuilder.nimble index 91ecf61..23ff4a9 100644 --- a/sqlbuilder.nimble +++ b/sqlbuilder.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.1" +version = "1.0.0" author = "ThomasTJdev" description = "SQL builder" license = "MIT" diff --git a/src/sqlbuilder.nim b/src/sqlbuilder.nim index 167d2a8..90e4955 100644 --- a/src/sqlbuilder.nim +++ b/src/sqlbuilder.nim @@ -1,373 +1,30 @@ # Copyright 2019 - Thomas T. Jarløv -## -## SQL builder -## ---------- -## -## SQL builder for ``INSERT``, ``UPDATE``, ``SELECT`` and ``DELETE`` queries. -## The builder will check for NULL values and build a query with them. -## -## After Nim's update to 0.19.0, the check for NULL values has been removed -## due to the removal of ``nil``. This library's main goal is to allow the -## user, to insert NULL values into the database again. -## -## This packages uses Nim's standard packages, e.g. db_postgres, -## proc to escape qoutes. -## -## -## Macro generated queries -## -------- -## -## The library supports generating the queries with a macro, which improves the -## performance due to query being generated on compile time. The macro generated -## queries **do not** accept the `genArgs()` - so there's currently not NULL- -## support. -## -## -## NULL values -## ------- -## -## -## A NULL value -## ============ -## -## The global ``var dbNullVal`` represents a NULL value. Use ``dbNullVal`` -## in your args, if you need to insert/update to a NULL value. -## -## Insert value or NULL -## ============ -## -## The global ``proc dbValOrNull()`` will check, if it's contain a value -## or is empty. If it contains a value, the value will be used in the args, -## otherwise a NULL value (``dbNullVal``) will be used. -## -## ``dbValOrNull()`` accepts both strings and int. -## -## -## Executing DB commands -## ============ -## -## The examples below support the various DB commands such as ``exec``, -## ``tryExec``, ``insertID``, ``tryInsertID``, etc. -## -## -## Examples (NULL values) -## ------- -## -## -## All the examples uses a table named: ``myTable`` and they use the WHERE argument on: ``name``. -## -## Update string & int -## ===================== -## -## ### Version 1 -## *Required if NULL values could be expected* -## .. code-block:: Nim -## let a = genArgs("em@em.com", 20, "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> string, int -## # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? -## -## -## ### Version 2 -## .. code-block:: Nim -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"]), "em@em.com", 20, "John") -## # ==> string, int -## # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? -## -## Update NULL & int -## ===================== -## -## .. code-block:: Nim -## let a = genArgs("", 20, "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> NULL, int -## # ==> UPDATE myTable SET email = NULL, age = ? WHERE name = ? -## -## Update string & NULL -## ===================== -## -## .. code-block:: Nim -## a = genArgs("aa@aa.aa", dbNullVal, "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> string, NULL -## # ==> UPDATE myTable SET email = ?, age = NULL WHERE name = ? -## -## Error: Update string & NULL -## ===================== -## -## An empty string, "", will be inserted into the database as NULL. -## Empty string cannot be used for an INTEGER column. You therefore -## need to use the ``dbValOrNull()`` or ``dbNullVal`` for ``int-values``. -## -## .. code-block:: Nim -## a = genArgs("aa@aa.aa", "", "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> string, ERROR -## # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? -## # ==> To insert a NULL into a int-field, it is required to use dbValOrNull() -## # or dbNullVal, it is only possible to pass and empty string. -## -## Update NULL & NULL -## ===================== -## -## .. code-block:: Nim -## let cc = "" -## a = genArgs(dbValOrNull(cc), dbValOrNull(cc), "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> NULL, NULL -## # ==> UPDATE myTable SET email = NULL, age = NULL WHERE name = ? -## -## -## Update unknow value - maybe NULL -## ===================== -## -## .. code-block:: Nim -## a = genArgs(dbValOrNull(stringVar), dbValOrNull(intVar), "John") -## exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) -## # ==> NULL, NULL -or- STRING, INT -## -## -## Examples (INSERT) -## ------- -## -## Insert without NULL -## ===================== -## -## .. code-block:: Nim -## exec(db, sqlInsert("myTable", ["email", "age"]), "em@em.com" , 20) -## # OR -## insertID(db, sqlInsert("myTable", ["email", "age"]), "em@em.com", 20) -## # ==> INSERT INTO myTable (email, age) VALUES (?, ?) -## -## Insert with NULL -## ===================== -## -## .. code-block:: Nim -## let a = genArgs("em@em.com", dbNullVal) -## exec(db, sqlInsert("myTable", ["email", "age"], a.query), a.args) -## # OR -## insertID(db, sqlInsert("myTable", ["email", "age"], a.query), a.args) -## # ==> INSERT INTO myTable (email) VALUES (?) -## -## Examples (SELECT) -## ------- -## -## Select without NULL -## ===================== -## -## .. code-block:: Nim -## getValue(db, sqlSelect("myTable", -## ["email", "age"], [""], ["name ="], "", "", ""), "John") -## # SELECT email, age FROM myTable WHERE name = ? -## -## getValue(db, sqlSelect("myTable", -## ["myTable.email", "myTable.age", "company.name"], -## ["company ON company.email = myTable.email"], -## ["myTable.name =", "myTable.age ="], "", "", ""), -## "John", "20") -## # SELECT myTable.email, myTable.age, company.name -## # FROM myTable -## # LEFT JOIN company ON company.email = myTable.email -## # WHERE myTable.name = ? AND myTable.age = ? -## -## getAllRows(db, sqlSelect("myTable", -## ["myTable.email", "myTable.age", "company.name"], -## ["company ON company.email = myTable.email"], -## ["company.name ="], "20,22,24", "myTable.age", "ORDER BY myTable.email"), -## "BigBiz") -## # SELECT myTable.email, myTable.age, company.name -## # FROM myTable LEFT JOIN company ON company.email = myTable.email -## # WHERE company.name = ? AND myTable.age IN (20,22,24) -## # ORDER BY myTable.email -## -## Select with NULL -## ===================== -## -## .. code-block:: Nim -## let a = genArgs(dbNullVal) -## getValue(db, sqlSelect("myTable", -## ["email", "age"], [""], ["name ="], "", "", "", a.query), a.args) -## # SELECT email, age FROM myTable WHERE name = NULL -## -## let a = genArgs("John", dbNullVal) -## getValue(db, sqlSelect("myTable", -## ["myTable.email", "myTable.age", "company.name"], -## ["company ON company.email = myTable.email"], -## ["myTable.name =", "myTable.age ="], "", "", "", a.query), a.args) -## # SELECT myTable.email, myTable.age, company.name -## # FROM myTable -## # LEFT JOIN company ON company.email = myTable.email -## # WHERE myTable.name = ? AND myTable.age = NULL -## -## let a = genArgs(dbNullVal) -## getAllRows(db, sqlSelect("myTable", -## ["myTable.email", "myTable.age", "company.name"], -## ["company ON company.email = myTable.email"], -## ["company.name ="], "20,22,24", "myTable.age", "ORDER BY myTable.email", a.query), -## a.args) -## # SELECT myTable.email, myTable.age, company.name -## # FROM myTable LEFT JOIN company ON company.email = myTable.email -## # WHERE company.name = NULL AND myTable.age IN (20,22,24) -## # ORDER BY myTable.email -## -## # Credit -## Inspiration for builder: [Nim Forum](https://github.com/nim-lang/nimforum) - -import - std/db_common, - std/macros, - std/strutils - -type - ArgObj* = object ## Argument object - val: string - isNull: bool - - ArgsContainer* = object ## Argument container used for queries and args - query*: seq[ArgObj] - args*: seq[string] - - ArgsFormat = object - use: bool - column: string - value: ArgObj - - -when defined(test): - var testout*: string - - -const dbNullVal* = ArgObj(isNull: true) ## Global NULL value - - -proc argType*(v: ArgObj): ArgObj = - ## Checks if a ``ArgObj`` is NULL and return - ## ``dbNullVal``. If it's not NULL, the passed - ## ``ArgObj`` is returned. - if v.isNull: - return dbNullVal - else: - return v - - -proc argType*(v: string | int): ArgObj = - ## Transforms a string or int to a ``ArgObj`` - var arg: ArgObj - arg.isNull = false - arg.val = $v - return arg - - -proc argTypeSetNull*(v: ArgObj): ArgObj = - if v.isNull: - return dbNullVal - elif v.val.len() == 0: - return dbNullVal - else: - return v - - -proc argTypeSetNull*(v: string | int): ArgObj = - var arg: ArgObj - if len($v) == 0: - return dbNullVal - else: - arg.isNull = false - arg.val = $v - return arg - - -proc dbValOrNull*(v: string | int): ArgObj = - ## Return NULL obj if len() == 0, else return value obj - if len($v) == 0: - return dbNullVal - var arg: ArgObj - arg.val = $v - arg.isNull = false - return arg - - -proc argFormat*(v: tuple): ArgsFormat = - ## Formats the tuple, so int, float, bool, etc. can be used directly. - result.use = v[0] - result.column = v[1] - result.value = argType(v[2]) - - -template genArgs*[T](arguments: varargs[T, argType]): ArgsContainer = - ## Create argument container for query and passed args. This allows for - ## using NULL in queries. - var argsContainer: ArgsContainer - argsContainer.query = @[] - argsContainer.args = @[] - for arg in arguments: - let argObject = argType(arg) - if argObject.isNull: - argsContainer.query.add(argObject) - else: - argsContainer.query.add(argObject) - argsContainer.args.add(argObject.val) - argsContainer - - -template genArgsSetNull*[T](arguments: varargs[T, argType]): ArgsContainer = - ## Create argument container for query and passed args - var argsContainer: ArgsContainer - argsContainer.query = @[] - argsContainer.args = @[] - for arg in arguments: - let argObject = argTypeSetNull(arg) - if argObject.isNull: - argsContainer.query.add(argObject) - else: - argsContainer.query.add(argObject) - argsContainer.args.add(argObject.val) - argsContainer - - -template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq[string], args: ArgsContainer] = - ## Create argument container for query and passed args and selecting which - ## columns to update. It's and expansion of `genArgs()`, since you can - ## decide which columns, there should be included. - ## - ## - ## Lets say, that you are importing data from a spreadsheet with static - ## columns, and you need to update your DB with the new values. - ## - ## If the spreadsheet contains an empty field, you can update your DB with the - ## NULL value. But sometimes the spreadsheet only contains 5 out of the 10 - ## static columns. Now you don't know the values of the last 5 columns, - ## which will result in updating the DB with NULL values. - ## - ## This template allows you to use the same query, but dynamic selecting only the - ## columns which shall be updated. When importing your spreadsheet, check - ## if the column exists (bool), and pass that as the `use: bool` param. If - ## the column does not exists, it will be skipped in the query. - var - select: seq[string] - argsContainer: ArgsContainer - argsContainer.query = @[] - argsContainer.args = @[] - for arg in arguments: - let argObject = argType(arg.value) - if not arg.use: - continue - if arg.column != "": - select.add(arg.column) - if argObject.isNull: - argsContainer.query.add(argObject) - else: - argsContainer.query.add(argObject) - argsContainer.args.add(argObject.val) - (select, argsContainer) - - -include sqlbuilderpkg/insert -include sqlbuilderpkg/update -include sqlbuilderpkg/delete -include sqlbuilderpkg/select + + + +import sqlbuilderpkg/insert +export insert + +import sqlbuilderpkg/update +export update + +import sqlbuilderpkg/delete +export delete + +import sqlbuilderpkg/select +export select + +# import sqlbuilderpkg/select_legacy +# export select_legacy + +import sqlbuilderpkg/utils +export utils + + + +#[ when isMainModule: from times import epochTime let a = epochTime() @@ -445,4 +102,6 @@ when isMainModule: echo "Delete:" echo "Macro: " & $(dm2-dm1) echo "Normal: " & $(dp2-dp1) - echo "Diff: " & $((dp2-dp1) - (dm2-dm1)) \ No newline at end of file + echo "Diff: " & $((dp2-dp1) - (dm2-dm1)) + +]# \ No newline at end of file diff --git a/src/sqlbuilderpkg/delete.nim b/src/sqlbuilderpkg/delete.nim index 43a5244..461aac8 100644 --- a/src/sqlbuilderpkg/delete.nim +++ b/src/sqlbuilderpkg/delete.nim @@ -1,5 +1,15 @@ # Copyright 2020 - Thomas T. Jarløv + +import + std/db_common, + std/macros, + std/strutils + +import + ./utils + + proc sqlDelete*(table: string, where: varargs[string]): SqlQuery = ## SQL builder for DELETE queries ## Does NOT check for NULL values diff --git a/src/sqlbuilderpkg/insert.nim b/src/sqlbuilderpkg/insert.nim index c2b935a..6047aec 100644 --- a/src/sqlbuilderpkg/insert.nim +++ b/src/sqlbuilderpkg/insert.nim @@ -1,6 +1,15 @@ # Copyright 2020 - Thomas T. Jarløv +import + std/db_common, + std/macros, + std/strutils + +import + ./utils + + proc sqlInsert*(table: string, data: varargs[string], args: ArgsContainer.query): SqlQuery = ## SQL builder for INSERT queries ## Checks for NULL values diff --git a/src/sqlbuilderpkg/select.nim b/src/sqlbuilderpkg/select.nim index a8cc0d8..f8e1671 100644 --- a/src/sqlbuilderpkg/select.nim +++ b/src/sqlbuilderpkg/select.nim @@ -1,5 +1,383 @@ # Copyright 2020 - Thomas T. Jarløv + +import + std/db_common, + std/macros, + std/strutils + +import + ./utils + + + +macro sqlSelectConst*( + # BASE + table: string, + select: varargs[string], + where: varargs[string], + # Join + joinargs: static varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], + jointype: SQLJoinType = SQLJoinType.LEFT, + # WHERE-IN + whereInField: string = "", + whereInValue: varargs[string] = [], + # Custom SQL, e.g. ORDER BY + customSQL: string = "", + # Table alias + tableAs: string = "",# = $table, + # Soft delete + hideIsDeleted: bool = true, + tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + deleteMarker = ".is_deleted IS NULL", + testValue: string = "", + ): SqlQuery = + ## SQL builder for SELECT queries + + + # + # Select + var res = "SELECT " + for i, d in select: + if i > 0: res.add(", ") + res.add($d) + + + # + # Joins + var lef = "" + + + for d in joinargs: + if d.repr.len == 0 and joinargs.len() == 2: + break + + lef.add(" " & $jointype & " JOIN ") + lef.add($d.table & " ") + + if d.tableAs != "" and d.tableAs != d.table: + lef.add("AS " & $d.tableAs & " ") + + lef.add("ON (") + + for i, join in d.on: + if i > 0: + lef.add(" AND ") + lef.add($join) + + if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + var hit = false + for t in tablesWithDeleteMarker: + if $t == $d.table: + hit = true + break + + if hit: + lef.add(" AND " & (if d.tableAs.len() > 0 and $d.tableAs != $d.table: $d.tableAs else: $d.table) & $deleteMarker) + + lef.add(")") + + + # + # Where + var wes = "" + for i, d in where: + let v = $d + + if v.len() > 0 and i == 0: + wes.add(" WHERE ") + + if i > 0: + wes.add(" AND ") + + if v.len() > 0: + wes.add(v & " ?") + + + + var acc = "" + if ($whereInField).len() > 0 and (whereInValue).len() > 0: + if wes.len == 0: + acc.add(" WHERE " & $whereInField & " in (") + else: + acc.add(" AND " & $whereInField & " in (") + + + var inVal: string + + for a in whereInValue: + if inVal != "": + inVal.add(",") + inVal.add($a) + + acc.add(if inVal == "": "0" else: inVal) + acc.add(")") + + + + # + # Soft delete + if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + var hit = false + for t in tablesWithDeleteMarker: + if $t == $table: + hit = true + break + + if hit: + let tableNameToUse = + if ($tableAs).len() > 0 and $tableAs != $table: + $tableAs + else: + $table + + if wes == "" and acc == "": + wes.add(" WHERE " & $tableNameToUse & $deleteMarker) + elif acc != "": + acc.add(" AND " & $tableNameToUse & $deleteMarker) + else: + wes.add(" AND " & $tableNameToUse & $deleteMarker) + + + + # + # Base + let tableName = + if ($tableAs).len() > 0 and $table != $tableAs: + $table & " AS " & $tableAs + else: + $table + + + + when defined(verboseSqlquery): + echo "SQL Macro:" + echo res & " FROM " & tableName & lef & wes & acc & " " & $customSQL + + + result = parseStmt("sql(\"" & res & " FROM " & tableName & lef & wes & acc & " " & $customSQL & "\")") + + + + +proc sqlSelect*( + # BASE + table: string, + select: varargs[string], + where: varargs[string], + # Join + joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], + jointype: SQLJoinType = LEFT, + joinoverride: string = "", + # WHERE-IN + whereInField: string = "", + whereInValue: seq[string] = @[], + whereInValueString: seq[string] = @[], + whereInValueInt: seq[int] = @[], + # Custom SQL, e.g. ORDER BY + customSQL: string = "", + # Null checks + checkedArgs: ArgsContainer.query = @[], + # Table alias + tableAs: string = table, + # Soft delete + hideIsDeleted: bool = true, + tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + deleteMarker = ".is_deleted IS NULL", + ): SqlQuery = + ## SQL builder for SELECT queries + + + # + # Select + var res = "SELECT " + for i, d in select: + if i > 0: res.add(", ") + res.add(d) + + + # + # Joins + var lef = "" + for i, d in joinargs: + lef.add(" " & $jointype & " JOIN ") + lef.add(d.table & " ") + + if d.tableAs != "" and d.tableAs != d.table: + lef.add("AS " & d.tableAs & " ") + + lef.add("ON (") + for i, join in d.on: + if i > 0: + lef.add(" AND ") + lef.add(join) + + if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and d.table in tablesWithDeleteMarker: + lef.add(" AND " & (if d.tableAs != "" and d.tableAs != d.table: d.tableAs else: d.table) & deleteMarker) + lef.add(")") + + if joinoverride.len() > 0: + lef.add(" " & joinoverride) + + # + # Where + var wes = "" + for i, d in where: + if d != "" and i == 0: + wes.add(" WHERE ") + + if i > 0: + wes.add(" AND ") + + if d != "": + if checkedArgs.len() > 0 and checkedArgs[i].isNull: + wes.add(d & " NULL") + else: + wes.add(d & " ?") + + + # + # Where IN + var acc = "" + if whereInField != "" and (whereInValue.len() > 0 or whereInValueString.len() > 0 or whereInValueInt.len() > 0): + if wes.len == 0: + acc.add(" WHERE " & whereInField & " in (") + else: + acc.add(" AND " & whereInField & " in (") + + var inVal: string + + if whereInValue.len() > 0: + inVal.add(whereInValue.join(",")) + + elif whereInValueString.len() > 0: + for a in whereInValueString: + if a == "": + continue + + if inVal != "": + inVal.add(",") + + inVal.add("'" & a & "'") + + else: + for a in whereInValueInt: + if inVal != "": + inVal.add(",") + inVal.add($a) + + acc.add(if inVal == "": "0" else: inVal) + acc.add(")") + + + + + # + # Soft delete + if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and table in tablesWithDeleteMarker: + let tableNameToUse = + if tableAs.len() > 0 and tableAs != table: + tableAs + else: + table + + if wes == "" and acc == "": + wes.add(" WHERE " & tableNameToUse & deleteMarker) + elif acc != "": + acc.add(" AND " & tableNameToUse & deleteMarker) + else: + wes.add(" AND " & tableNameToUse & deleteMarker) + + + # + # Alias + let tableName = + if tableAs != "" and table != tableAs: + table & " AS " & tableAs + else: + table + + + # + # Finalize + when defined(verboseSqlquery): + echo res & " FROM " & tableName & lef & wes & acc & " " & customSQL + + + result = sql(res & " FROM " & tableName & lef & wes & acc & " " & customSQL) + + + + + + + +proc sqlSelect*( + table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, + args: ArgsContainer.query = @[], + hideIsDeleted: bool = true, + tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + deleteMarker = ".is_deleted IS NULL", + ): SqlQuery {.deprecated.} = + + + var leftcon: seq[tuple[table: string, tableAs: string, on: seq[string]]] + var joinoverride: string + for raw in left: + if raw.len() == 0: + continue + + if raw.len() >= 7 and raw[0..6].toLowerAscii() == "lateral": + joinoverride = "LEFT JOIN " & raw + continue + + let d = raw#.toLowerAscii() + + let + lsplit1 = if d.contains(" ON "): d.split(" ON ") else: d.split(" on ") + lsplit2 = if lsplit1[0].contains(" AS "): lsplit1[0].split(" AS ") else: lsplit1[0].split(" as ") + hasAlias = (lsplit2.len() > 1) + + leftcon.add( + ( + table: lsplit2[0].strip(), + tableAs: (if hasAlias: lsplit2[1].strip() else: ""), + on: @[lsplit1[1].strip()] + ) + ) + + let + tableSplit = if table.contains(" AS "): table.split(" AS ") else: table.split(" as ") #table.toLowerAscii().split(" as ") + tableName = + if tableSplit.len() > 1: + tableSplit[0] + else: + table + tableAsName = + if tableSplit.len() > 1: + tableSplit[1] + else: + "" + + return sqlSelect( + table = tableName, + tableAs = tableAsName, + select = data, + joinargs = leftcon, + jointype = LEFT, + joinoverride = joinoverride, + where = whereC, + whereInField = accessC, + whereInValue = @[access], + customSQL = user, + checkedArgs = args, + hideIsDeleted = hideIsDeleted, + tablesWithDeleteMarker = tablesWithDeleteMarker, + deleteMarker = deleteMarker + ) + + +#[ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = ## SQL builder for SELECT queries ## Does NOT check for NULL values @@ -8,11 +386,13 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe for i, d in data: if i > 0: res.add(", ") res.add(d) + var lef = "" for i, d in left: if d != "": lef.add(" LEFT JOIN ") lef.add(d) + var wes = "" for i, d in whereC: if d != "" and i == 0: @@ -21,6 +401,7 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe wes.add(" AND ") if d != "": wes.add(d & " ?") + var acc = "" if access != "": if wes.len == 0: @@ -54,11 +435,13 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe for i, d in data: if i > 0: res.add(", ") res.add(d) + var lef = "" for i, d in left: if d != "": lef.add(" LEFT JOIN ") lef.add(d) + var wes = "" for i, d in whereC: if d != "" and i == 0: @@ -70,6 +453,7 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe wes.add(d & " = NULL") else: wes.add(d & " ?") + var acc = "" if access != "": if wes.len == 0: @@ -77,6 +461,7 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe acc.add("(") else: acc.add(" AND " & accessC & " in (") + var inVal: string for a in split(access, ","): if a == "": continue @@ -93,9 +478,9 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe testout = res & " FROM " & table & lef & wes & acc & " " & user result = sql(res & " FROM " & table & lef & wes & acc & " " & user) +]# - -macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = +macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery {.deprecated.} = ## SQL builder for SELECT queries ## Does NOT check for NULL values @@ -135,4 +520,5 @@ macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string when defined(testSqlquery): echo "SELECT " & res & " FROM " & $table & lef & wes & acc & " " & $user - result = parseStmt("sql(\"SELECT " & res & " FROM " & $table & lef & wes & acc & " " & $user & "\")") \ No newline at end of file + result = parseStmt("sql(\"SELECT " & res & " FROM " & $table & lef & wes & acc & " " & $user & "\")") + diff --git a/src/sqlbuilderpkg/select_legacy.nim b/src/sqlbuilderpkg/select_legacy.nim new file mode 100644 index 0000000..da764c1 --- /dev/null +++ b/src/sqlbuilderpkg/select_legacy.nim @@ -0,0 +1,232 @@ +# Copyright 2020 - Thomas T. Jarløv + + +import + std/db_common, + std/macros, + std/strutils + +import + ./utils + +import + ./select + + + + +proc sqlSelect*( + table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, + args: ArgsContainer.query = @[], + hideIsDeleted: bool = true, + tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + deleteMarker = ".is_deleted IS NULL", + ): SqlQuery {.deprecated.} = + ## + ## This is a legacy converter for the old sqlSelect() use. + ## + + + var leftcon: seq[tuple[table: string, tableAs: string, on: seq[string]]] + var joinoverride: string + for raw in left: + if raw.len() == 0: + continue + + if raw.len() >= 7 and raw[0..6].toLowerAscii() == "lateral": + joinoverride = "LEFT JOIN " & raw + continue + + let d = raw.toLowerAscii() + + let + lsplit1 = d.split(" on ") + lsplit2 = lsplit1[0].split(" as ") + hasAlias = (lsplit2.len() > 1) + + leftcon.add( + ( + table: lsplit2[0].strip(), + tableAs: (if hasAlias: lsplit2[1].strip() else: ""), + on: @[lsplit1[1].strip()] + ) + ) + + let + tableSplit = table.split(" as ") + tableName = + if tableSplit.len() > 1: + tableSplit[0] + else: + table + tableAsName = + if tableSplit.len() > 1: + tableSplit[1] + else: + "" + + + + return sqlSelect( + table = tableName, + tableAs = tableAsName, + select = data, + joinargs = leftcon, + jointype = LEFT, + joinoverride = joinoverride, + where = whereC, + whereInField = accessC, + whereInValue = @[access], + customSQL = user, + checkedArgs = args, + hideIsDeleted = hideIsDeleted, + tablesWithDeleteMarker = tablesWithDeleteMarker, + deleteMarker = deleteMarker + ) + + +#[ +proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = + ## SQL builder for SELECT queries + ## Does NOT check for NULL values + + var res = "SELECT " + for i, d in data: + if i > 0: res.add(", ") + res.add(d) + + var lef = "" + for i, d in left: + if d != "": + lef.add(" LEFT JOIN ") + lef.add(d) + + var wes = "" + for i, d in whereC: + if d != "" and i == 0: + wes.add(" WHERE ") + if i > 0: + wes.add(" AND ") + if d != "": + wes.add(d & " ?") + + var acc = "" + if access != "": + if wes.len == 0: + acc.add(" WHERE " & accessC & " in ") + acc.add("(") + else: + acc.add(" AND " & accessC & " in (") + var inVal: string + for a in split(access, ","): + if a == "": continue + if inVal != "": + inVal.add(",") + inVal.add(a) + acc.add(if inVal == "": "0" else: inVal) + acc.add(")") + + when defined(testSqlquery): + echo res & " FROM " & table & lef & wes & acc & " " & user + + when defined(test): + testout = res & " FROM " & table & lef & wes & acc & " " & user + + result = sql(res & " FROM " & table & lef & wes & acc & " " & user) + + +proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, args: ArgsContainer.query): SqlQuery = + ## SQL builder for SELECT queries + ## Checks for NULL values + + var res = "SELECT " + for i, d in data: + if i > 0: res.add(", ") + res.add(d) + + var lef = "" + for i, d in left: + if d != "": + lef.add(" LEFT JOIN ") + lef.add(d) + + var wes = "" + for i, d in whereC: + if d != "" and i == 0: + wes.add(" WHERE ") + if i > 0: + wes.add(" AND ") + if d != "": + if args[i].isNull: + wes.add(d & " = NULL") + else: + wes.add(d & " ?") + + var acc = "" + if access != "": + if wes.len == 0: + acc.add(" WHERE " & accessC & " in ") + acc.add("(") + else: + acc.add(" AND " & accessC & " in (") + + var inVal: string + for a in split(access, ","): + if a == "": continue + if inVal != "": + inVal.add(",") + inVal.add(a) + acc.add(if inVal == "": "0" else: inVal) + acc.add(")") + + when defined(testSqlquery): + echo res & " FROM " & table & lef & wes & acc & " " & user + + when defined(test): + testout = res & " FROM " & table & lef & wes & acc & " " & user + + result = sql(res & " FROM " & table & lef & wes & acc & " " & user) +]# + +macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery {.deprecated.} = + ## SQL builder for SELECT queries + ## Does NOT check for NULL values + + var res: string + for i, d in data: + if i > 0: + res.add(", ") + res.add($d) + + var lef: string + for i, d in left: + if $d != "": + lef.add(" LEFT JOIN " & $d) + + var wes: string + for i, d in whereC: + if $d != "" and i == 0: + wes.add(" WHERE ") + if i > 0: + wes.add(" AND ") + if $d != "": + wes.add($d & " ?") + + var acc: string + if access.len() != 0: + if wes.len == 0: + acc.add(" WHERE " & $accessC & " in (") + else: + acc.add(" AND " & $accessC & " in (") + for a in split($access, ","): + acc.add(a & ",") + acc = acc[0 .. ^2] + if acc.len() == 0: + acc.add("0") + acc.add(")") + + when defined(testSqlquery): + echo "SELECT " & res & " FROM " & $table & lef & wes & acc & " " & $user + + result = parseStmt("sql(\"SELECT " & res & " FROM " & $table & lef & wes & acc & " " & $user & "\")") + diff --git a/src/sqlbuilderpkg/update.nim b/src/sqlbuilderpkg/update.nim index ac4e760..0be7e8e 100644 --- a/src/sqlbuilderpkg/update.nim +++ b/src/sqlbuilderpkg/update.nim @@ -1,6 +1,15 @@ # Copyright 2020 - Thomas T. Jarløv +import + std/db_common, + std/macros, + std/strutils + +import + ./utils + + proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], args: ArgsContainer.query): SqlQuery = ## SQL builder for UPDATE queries ## Checks for NULL values @@ -13,6 +22,7 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], ar fields.add(d & " = NULL") else: fields.add(d & " = ?") + var wes = " WHERE " for i, d in where: if i > 0: @@ -37,6 +47,7 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string]): S if i > 0: fields.add(", ") fields.add(d & " = ?") + var wes = " WHERE " for i, d in where: if i > 0: diff --git a/src/sqlbuilderpkg/utils.nim b/src/sqlbuilderpkg/utils.nim new file mode 100644 index 0000000..b71aa7c --- /dev/null +++ b/src/sqlbuilderpkg/utils.nim @@ -0,0 +1,164 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + + +type + ArgObj* = object ## Argument object + val*: string + isNull*: bool + + ArgsContainer* = object ## Argument container used for queries and args + query*: seq[ArgObj] + args*: seq[string] + + ArgsFormat = object + use: bool + column: string + value: ArgObj + + SQLJoinType* = enum + INNER + LEFT + RIGHT + FULL + + SQLJoinObject* = ref object + joinType*: SQLJoinType + table*: string + tableAs*: string + on*: seq[string] + + +when defined(test): + var testout*: string + + +const dbNullVal* = ArgObj(isNull: true) ## Global NULL value + + +proc argType*(v: ArgObj): ArgObj = + ## Checks if a ``ArgObj`` is NULL and return + ## ``dbNullVal``. If it's not NULL, the passed + ## ``ArgObj`` is returned. + if v.isNull: + return dbNullVal + else: + return v + + +proc argType*(v: string | int): ArgObj = + ## Transforms a string or int to a ``ArgObj`` + var arg: ArgObj + arg.isNull = false + arg.val = $v + return arg + + +proc argTypeSetNull*(v: ArgObj): ArgObj = + if v.isNull: + return dbNullVal + elif v.val.len() == 0: + return dbNullVal + else: + return v + + +proc argTypeSetNull*(v: string | int): ArgObj = + var arg: ArgObj + if len($v) == 0: + return dbNullVal + else: + arg.isNull = false + arg.val = $v + return arg + + +proc dbValOrNullString*(v: string | int): string = + ## Return NULL obj if len() == 0, else return value obj + if len($v) == 0: + return "" + return v + + +proc dbValOrNull*(v: string | int): ArgObj = + ## Return NULL obj if len() == 0, else return value obj + if len($v) == 0: + return dbNullVal + var arg: ArgObj + arg.val = $v + arg.isNull = false + return arg + + +proc argFormat*(v: tuple): ArgsFormat = + ## Formats the tuple, so int, float, bool, etc. can be used directly. + result.use = v[0] + result.column = v[1] + result.value = argType(v[2]) + + +template genArgs*[T](arguments: varargs[T, argType]): ArgsContainer = + ## Create argument container for query and passed args. This allows for + ## using NULL in queries. + var argsContainer: ArgsContainer + argsContainer.query = @[] + argsContainer.args = @[] + for arg in arguments: + let argObject = argType(arg) + if argObject.isNull: + argsContainer.query.add(argObject) + else: + argsContainer.query.add(argObject) + argsContainer.args.add(argObject.val) + argsContainer + + +template genArgsSetNull*[T](arguments: varargs[T, argType]): ArgsContainer = + ## Create argument container for query and passed args + var argsContainer: ArgsContainer + argsContainer.query = @[] + argsContainer.args = @[] + for arg in arguments: + let argObject = argTypeSetNull(arg) + if argObject.isNull: + argsContainer.query.add(argObject) + else: + argsContainer.query.add(argObject) + argsContainer.args.add(argObject.val) + argsContainer + + +template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq[string], args: ArgsContainer] = + ## Create argument container for query and passed args and selecting which + ## columns to update. It's and expansion of `genArgs()`, since you can + ## decide which columns, there should be included. + ## + ## + ## Lets say, that you are importing data from a spreadsheet with static + ## columns, and you need to update your DB with the new values. + ## + ## If the spreadsheet contains an empty field, you can update your DB with the + ## NULL value. But sometimes the spreadsheet only contains 5 out of the 10 + ## static columns. Now you don't know the values of the last 5 columns, + ## which will result in updating the DB with NULL values. + ## + ## This template allows you to use the same query, but dynamic selecting only the + ## columns which shall be updated. When importing your spreadsheet, check + ## if the column exists (bool), and pass that as the `use: bool` param. If + ## the column does not exists, it will be skipped in the query. + var + select: seq[string] + argsContainer: ArgsContainer + argsContainer.query = @[] + argsContainer.args = @[] + for arg in arguments: + let argObject = argType(arg.value) + if not arg.use: + continue + if arg.column != "": + select.add(arg.column) + if argObject.isNull: + argsContainer.query.add(argObject) + else: + argsContainer.query.add(argObject) + argsContainer.args.add(argObject.val) + (select, argsContainer) \ No newline at end of file diff --git a/tests/test.nim b/tests/test.nim index 43767c1..28df9d2 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -2,9 +2,27 @@ import std/db_common, + std/strutils, std/unittest, src/sqlbuilder +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + suite "test formats": @@ -99,13 +117,465 @@ suite "test formats": test "sqlSelect": let a2 = genArgsSetNull("hje", "", "123") - discard sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a2.query) - assert testout == "SELECT name, age FROM my-table WHERE id = ? " + let q1 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a2.query) + check querycompare(q1, sql"SELECT name, age FROM my-table WHERE id = ? ") let a3 = genArgs("hje", "") - discard sqlSelect("my-table AS m", ["m.name", "m.age"], ["p ON p.id = m.id"], ["m.id ="], "", "", "", a3.query) - assert testout == "SELECT m.name, m.age FROM my-table AS m LEFT JOIN p ON p.id = m.id WHERE m.id = ? " + let q2 = sqlSelect("my-table AS m", ["m.name", "m.age"], ["p ON p.id = m.id"], ["m.id ="], "", "", "", a3.query) + check querycompare(q2, sql"SELECT m.name, m.age FROM my-table AS m LEFT JOIN p ON (p.id = m.id) WHERE m.id = ? ") let a4 = genArgs("hje", dbNullVal) - discard sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a4.query) - assert testout == "SELECT name, age FROM my-table WHERE id = ? " \ No newline at end of file + let q3 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a4.query) + check querycompare(q3, sql"SELECT name, age FROM my-table WHERE id = ? ") + + +suite "test sqlSelect": + + test "sqlSelect - refactor 2022-01": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + jointype = INNER, + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + + let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid AND history.is_deleted IS NULL) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet, + deleteMarker = ".deleted_at = 543234563" + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid AND history.deleted_at = 543234563) WHERE id = ? AND tasks.deleted_at = 543234563 ")) + + + + test = sqlSelect( + table = "tasks", + select = @["tasks.id", "tasks.name"], + where = @["tasks.id ="], + joinargs = @[(table: "history", tableAs: "his", on: @["his.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT tasks.id, tasks.name FROM tasks LEFT JOIN history AS his ON (his.id = tasks.hid AND his.is_deleted IS NULL) WHERE tasks.id = ? AND tasks.is_deleted IS NULL ")) + + + + test = sqlSelect( + table = "tasksitems", + tableAs = "tasks", + select = @[ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + where = @[ + "projects.id =", + "tasks.status >" + ], + joinargs = @[ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) + ], + whereInField = "tasks.id", + whereInValue = @["1", "2", "3"], + customSQL = "ORDER BY tasks.created DESC", + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1 AND his.is_deleted IS NULL) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + AND tasks.is_deleted IS NULL + ORDER BY + tasks.created DESC + """))) + + +suite "test sqlSelectMacro": + + test "sqlSelectMacro legacy - refactor 2022-01": + + let q1 = sqlSelectMacro( + table = "my-table", + data = ["name", "age"], + left = [""], + whereC = ["id ="], "", "", "") + + check querycompare(q1, sql("SELECT name, age FROM my-table WHERE id = ?")) + + +suite "test sqlSelectConst": + + test "sqlSelectConst - refactor 2022-01": + let a = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker + ) + + check querycompare(a, (sql(""" + SELECT + t.id, t.name, t.description, t.created, t.updated, t.completed + FROM + tasks AS t + WHERE + t.id = ? + AND t.is_deleted IS NULL + """))) + + + let b = sqlSelectConst( + table = "tasks", + # tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "status >"], + joinargs = [ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] #tableWithDeleteMarker + ) + + check querycompare(b, sql(""" + SELECT + id, name, description, created, updated, completed + FROM + tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1 AND his.is_deleted IS NULL) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + WHERE + id = ? + AND status > ? + AND tasks.is_deleted IS NULL + """)) + + + let c = sqlSelectConst( + table = "tasks", + select = ["tasks.id"], + where = ["status >"], + joinargs = [ + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(c, sql(""" + SELECT + tasks.id + FROM + tasks + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + WHERE + status > ? + AND tasks.is_deleted IS NULL + """)) + + + let d = sqlSelectConst( + table = "tasks", + select = ["tasks.id"], + where = ["status >"], + joinargs = [ + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(d, sql(""" + SELECT + tasks.id + FROM + tasks + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + LEFT JOIN history ON + (his.id = tasks.hid AND history.is_deleted IS NULL) + WHERE + status > ? + AND tasks.is_deleted IS NULL + """)) + + + let e = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name"], + where = ["t.id ="], + whereInField = "t.name", + whereInValue = ["'1aa'", "'2bb'", "'3cc'"], + tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker + ) + check querycompare(e, sql(""" + SELECT + t.id, t.name + FROM + tasks AS t + WHERE + t.id = ? + AND t.name in ('1aa','2bb','3cc')""")) + + + let f = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name"], + where = ["t.id ="], + whereInField = "t.id", + whereInValue = [""], + tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker + ) + check querycompare(f, sql("SELECT t.id, t.name FROM tasks AS t WHERE t.id = ? AND t.id in (0)")) + + +suite "test sqlSelect(Convert) - legacy": + + test "sqlSelectConvert - refactor 2022-01": + + var test: SqlQuery + + + test = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "") + + check querycompare(test, sql("SELECT name, age FROM my-table WHERE id = ?")) + + + + test = sqlSelect("tasks", ["tasks.id", "tasks.name"], ["project ON project.id = tasks.project_id"], ["id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + tasks.id, + tasks.name + FROM + tasks + LEFT JOIN project ON + (project.id = tasks.project_id) + WHERE + id = ? + """)) + + + + test = sqlSelect("tasks", ["tasks.id", "tasks.name", "p.id"], ["project AS p ON p.id = tasks.project_id"], ["tasks.id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + tasks.id, + tasks.name, + p.id + FROM + tasks + LEFT JOIN project AS p ON + (p.id = tasks.project_id) + WHERE + tasks.id = ? + """)) + + + + test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + """)) + + + + test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + ORDER BY + t.name + """)) + + + + test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name", tablesWithDeleteMarker = ["tasks", "persons"]) + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "sqlSelectConvert with genArgs - refactor 2022-01": + + + var a = genArgs("123", dbNullVal) + + var test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NULL""")) + + + + a = genArgs("123", dbNullVal, dbNullVal) + + test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) + + + test "sqlSelectConvert with genArgsSetNull - refactor 2022-01": + + var a = genArgsSetNull("123", "", "") + + var test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) \ No newline at end of file diff --git a/tests/test2.nim b/tests/test2.nim new file mode 100644 index 0000000..eb52414 --- /dev/null +++ b/tests/test2.nim @@ -0,0 +1,118 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + + + +import src/sqlbuilderpkg/insert +export insert + +import src/sqlbuilderpkg/update +export update + +import src/sqlbuilderpkg/delete +export delete + +import src/sqlbuilderpkg/utils +export utils + +const tablesWithDeleteMarker = ["tasks", "persons"] + +include src/sqlbuilderpkg/select + + +import std/unittest + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + +suite "test sqlSelect": + + test "set tablesWithDeleteMarker": + + let test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) + + +suite "test sqlSelectConst": + + test "set tablesWithDeleteMarker": + + let a = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker + ) + + + +suite "test sqlSelect(converter) legacy": + + test "set tablesWithDeleteMarker": + + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "double delete": + + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id AND persons.is_deleted IS NULL AND persons.is_deleted IS NULL) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) \ No newline at end of file From cf06f9a1b38a8eddd40fc443da7d2127cf0356a2 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 19:57:55 +0200 Subject: [PATCH 02/27] Add tests to runner --- .github/workflows/main.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0f382f4..2f2cc4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,9 +13,8 @@ jobs: matrix: os: - ubuntu-latest - - windows-latest - - macOS-latest version: + - 1.6.14 - stable steps: @@ -30,11 +29,5 @@ jobs: - name: Print Nimble version run: nimble -v - #- name: Nimble Refresh - # run: nimble -y refresh - - #- name: Nimble Install dependencies - # run: nimble -y install --depsOnly - - name: Build binaries - run: nimble install -d:release \ No newline at end of file + run: nimble test \ No newline at end of file From 184e0ce41bc155492539ea59edfc9efe40925c23 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 19:58:26 +0200 Subject: [PATCH 03/27] Refactor code --- sqlbuilder.nimble | 2 + src/sqlbuilder.nim | 3 + src/sqlbuilder_include.nim | 15 + src/sqlbuilderpkg/delete.nim | 7 +- src/sqlbuilderpkg/insert.nim | 12 +- src/sqlbuilderpkg/query_calls.nim | 94 ++++ src/sqlbuilderpkg/select.nim | 778 +++++++++++++++++++++------- src/sqlbuilderpkg/select_legacy.nim | 9 +- src/sqlbuilderpkg/totypes.nim | 66 +++ src/sqlbuilderpkg/update.nim | 185 ++++++- src/sqlbuilderpkg/utils.nim | 2 +- src/sqlbuilderpkg/utils_private.nim | 13 + 12 files changed, 961 insertions(+), 225 deletions(-) create mode 100644 src/sqlbuilder_include.nim create mode 100644 src/sqlbuilderpkg/query_calls.nim create mode 100644 src/sqlbuilderpkg/totypes.nim create mode 100644 src/sqlbuilderpkg/utils_private.nim diff --git a/sqlbuilder.nimble b/sqlbuilder.nimble index 23ff4a9..8939282 100644 --- a/sqlbuilder.nimble +++ b/sqlbuilder.nimble @@ -10,3 +10,5 @@ srcDir = "src" # Dependencies requires "nim >= 0.20.2" +when NimMajor >= 2: + requires "db_connector >= 0.1.0" diff --git a/src/sqlbuilder.nim b/src/sqlbuilder.nim index 90e4955..f5bf4f6 100644 --- a/src/sqlbuilder.nim +++ b/src/sqlbuilder.nim @@ -19,6 +19,9 @@ export select # import sqlbuilderpkg/select_legacy # export select_legacy +import sqlbuilderpkg/totypes +export totypes + import sqlbuilderpkg/utils export utils diff --git a/src/sqlbuilder_include.nim b/src/sqlbuilder_include.nim new file mode 100644 index 0000000..f8faf6d --- /dev/null +++ b/src/sqlbuilder_include.nim @@ -0,0 +1,15 @@ +# Copyright 2019 - Thomas T. Jarløv + + + +include sqlbuilderpkg/utils + +include sqlbuilderpkg/insert + +include sqlbuilderpkg/update + +include sqlbuilderpkg/delete + +include sqlbuilderpkg/select + +include sqlbuilderpkg/totypes diff --git a/src/sqlbuilderpkg/delete.nim b/src/sqlbuilderpkg/delete.nim index 461aac8..ee282b6 100644 --- a/src/sqlbuilderpkg/delete.nim +++ b/src/sqlbuilderpkg/delete.nim @@ -1,8 +1,13 @@ # Copyright 2020 - Thomas T. Jarløv +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common import - std/db_common, std/macros, std/strutils diff --git a/src/sqlbuilderpkg/insert.nim b/src/sqlbuilderpkg/insert.nim index 6047aec..cb03398 100644 --- a/src/sqlbuilderpkg/insert.nim +++ b/src/sqlbuilderpkg/insert.nim @@ -1,8 +1,13 @@ # Copyright 2020 - Thomas T. Jarløv +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common import - std/db_common, std/macros, std/strutils @@ -41,10 +46,15 @@ proc sqlInsert*(table: string, data: varargs[string]): SqlQuery = var fields = "INSERT INTO " & table & " (" var vals = "" for i, d in data: + # New value if i > 0: fields.add(", ") vals.add(", ") + + # Insert field name fields.add(d) + + # Insert value parameter vals.add('?') when defined(testSqlquery): diff --git a/src/sqlbuilderpkg/query_calls.nim b/src/sqlbuilderpkg/query_calls.nim new file mode 100644 index 0000000..99f4db9 --- /dev/null +++ b/src/sqlbuilderpkg/query_calls.nim @@ -0,0 +1,94 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +## +## These are procs to catch DB errors and return a default value to move on. +## This should only be used if: +## - It is not critical data +## - You can live with a default value in case of an error +## - You have no other way to catch the error +## - You are to lazy to write the try-except procs yourself +## + +import + std/logging, + std/typetraits + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_postgres + + + +proc getValueTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = + try: + result = getValue(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +proc getAllRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[Row] = + try: + result = getAllRows(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +proc getRowTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = + try: + result = getRow(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +iterator fastRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = + try: + for i in fastRows(db, query, args): + yield i + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +proc tryExecTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): bool = + try: + result = tryExec(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +proc execTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]) = + try: + exec(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + + + + +proc execAffectedRowsTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = + try: + result = execAffectedRows(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + return -1 + + + +proc tryInsertIDTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = + try: + result = tryInsertID(db, query, args) + except: + error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + return -1 \ No newline at end of file diff --git a/src/sqlbuilderpkg/select.nim b/src/sqlbuilderpkg/select.nim index f8e1671..4a5d4df 100644 --- a/src/sqlbuilderpkg/select.nim +++ b/src/sqlbuilderpkg/select.nim @@ -1,56 +1,49 @@ # Copyright 2020 - Thomas T. Jarløv +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common import - std/db_common, std/macros, std/strutils import - ./utils + ./utils, + ./utils_private -macro sqlSelectConst*( - # BASE - table: string, - select: varargs[string], - where: varargs[string], - # Join - joinargs: static varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], - jointype: SQLJoinType = SQLJoinType.LEFT, - # WHERE-IN - whereInField: string = "", - whereInValue: varargs[string] = [], - # Custom SQL, e.g. ORDER BY - customSQL: string = "", - # Table alias - tableAs: string = "",# = $table, - # Soft delete - hideIsDeleted: bool = true, - tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], - deleteMarker = ".is_deleted IS NULL", - testValue: string = "", - ): SqlQuery = - ## SQL builder for SELECT queries - # - # Select - var res = "SELECT " +## +## Constant generator utilities +## +proc sqlSelectConstSelect(select: varargs[string]): string = + result = "SELECT " for i, d in select: - if i > 0: res.add(", ") - res.add($d) + if i > 0: result.add(", ") + result.add($d) - # - # Joins +proc sqlSelectConstJoin( + joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]], + jointype: NimNode, + # tablesWithDeleteMarker: varargs[string], + # hideIsDeleted: NimNode, + # deleteMarker: NimNode, + ): string = var lef = "" + if joinargs == [] or $(joinargs.repr) == "[]": + return for d in joinargs: if d.repr.len == 0 and joinargs.len() == 2: - break + continue lef.add(" " & $jointype & " JOIN ") lef.add($d.table & " ") @@ -65,22 +58,37 @@ macro sqlSelectConst*( lef.add(" AND ") lef.add($join) - if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: - var hit = false - for t in tablesWithDeleteMarker: - if $t == $d.table: - hit = true - break - - if hit: - lef.add(" AND " & (if d.tableAs.len() > 0 and $d.tableAs != $d.table: $d.tableAs else: $d.table) & $deleteMarker) + # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + # var hit = false + # for t in tablesWithDeleteMarker: + # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: + # continue + + # if $t == $d.table: + # hit = true + # break + + # if hit: + # lef.add( + # " AND " & + # ( + # if d.tableAs.len() > 0 and $d.tableAs != $d.table: + # $d.tableAs + # else: + # $d.table + # ) & + # $deleteMarker + # ) lef.add(")") + return lef + + +proc sqlSelectConstWhere(where: varargs[string]): string = - # - # Where var wes = "" + for i, d in where: let v = $d @@ -91,11 +99,36 @@ macro sqlSelectConst*( wes.add(" AND ") if v.len() > 0: - wes.add(v & " ?") + # => ... = NULL + if v[(v.high - 3)..v.high] == "NULL": + wes.add(v) + + # => ? = ANY(...) + elif v.len() > 5 and v[0..4] == "= ANY": + wes.add("? " & v) + + # => ... IN (?) + elif v[(v.high - 2)..v.high] == " IN": + wes.add(v & " (?)") + # => ? IN (...) + elif v.len() > 2 and v[0..1] == "IN": + wes.add("? " & v) + # => ... = ? + else: + wes.add(v & " ?") + + return wes + + +proc sqlSelectConstWhereIn( + wes, acc: string, + whereInField: NimNode, whereInValue: NimNode + ): string = var acc = "" + if ($whereInField).len() > 0 and (whereInValue).len() > 0: if wes.len == 0: acc.add(" WHERE " & $whereInField & " in (") @@ -113,35 +146,156 @@ macro sqlSelectConst*( acc.add(if inVal == "": "0" else: inVal) acc.add(")") + return acc - # - # Soft delete +proc sqlSelectConstSoft( + wes, acc: string, + # table, tableAs: NimNode, + tablesInQuery: seq[tuple[table: string, tableAs: string]], + tablesWithDeleteMarker: varargs[string], + hideIsDeleted: NimNode, + deleteMarker: NimNode + ): (string, string) = + if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: - var hit = false - for t in tablesWithDeleteMarker: - if $t == $table: - hit = true - break + var wesTo, accTo: string - if hit: - let tableNameToUse = - if ($tableAs).len() > 0 and $tableAs != $table: - $tableAs - else: - $table + for t in tablesInQuery: + if t.table notin tablesWithDeleteMarker: # and t.tableAs notin tablesWithDeleteMarker: + continue + + let toUse = if t.tableAs != "": t.tableAs else: t.table if wes == "" and acc == "": - wes.add(" WHERE " & $tableNameToUse & $deleteMarker) + wesTo.add(" WHERE " & toUse & $deleteMarker) + elif acc != "": - acc.add(" AND " & $tableNameToUse & $deleteMarker) + accTo.add(" AND " & toUse & $deleteMarker) + else: - wes.add(" AND " & $tableNameToUse & $deleteMarker) + wesTo.add(" AND " & toUse & $deleteMarker) + + return (wesTo, accTo) + + # var hit = false + # for t in tablesWithDeleteMarker: + # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: + # continue + + # if $t == $table: + # hit = true + # break + + + # if hit: + # let tableNameToUse = + # if ($tableAs).len() > 0 and $tableAs != $table: + # $tableAs + # else: + # $table + + # var wesTo, accTo: string + + # if wes == "" and acc == "": + # wesTo.add(" WHERE " & $tableNameToUse & $deleteMarker) + + # elif acc != "": + # accTo.add(" AND " & $tableNameToUse & $deleteMarker) + + # else: + # wesTo.add(" AND " & $tableNameToUse & $deleteMarker) + + # return (wesTo, accTo) + + +## +## Constant generator +## +const deadTuple: tuple[table: string, tableAs: string, on: seq[string]] = ("", "", @[]) +macro sqlSelectConst*( + # BASE + table: string, + select: static varargs[string], + where: static varargs[string], + + # Join + joinargs: static varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], + jointype: SQLJoinType = SQLJoinType.LEFT, + + # WHERE-IN + whereInField: string = "", + whereInValue: varargs[string] = [], + + # Custom SQL, e.g. ORDER BY + customSQL: string = "", + + # Table alias + tableAs: string = "",# = $table, + + # Soft delete + hideIsDeleted: bool = true, + tablesWithDeleteMarker: static varargs[string] = [], + deleteMarker = ".is_deleted IS NULL", + testValue: string = "", + ): SqlQuery = + ## SQL builder for SELECT queries + + + # + # Set delete fields + # + # 1) macro options + # 2) default options set outside macro + # + var deleteMarkersFields: seq[string] + if $(tablesWithDeleteMarker.repr) != "[]": + for t in tablesWithDeleteMarker: + if t.repr.len == 0:# or tablesWithDeleteMarker.len() == 2: + continue + if t notin deleteMarkersFields: + deleteMarkersFields.add(t) + + when declared(tablesWithDeleteMarkerInit): + if $(tablesWithDeleteMarker.repr) != "[]": + for t in tablesWithDeleteMarkerInit: + if t.repr.len == 0:# or tablesWithDeleteMarkerInit.len() == 2: + continue + if t notin deleteMarkersFields: + deleteMarkersFields.add(t) + # + # Create seq of tables + # + var tablesInQuery: seq[tuple[table: string, tableAs: string]] + + # Base table + if $tableAs != "" and $table != $tableAs: + tablesInQuery.add(($table, $tableAs)) + else: + tablesInQuery.add(($table, "")) + + # Join table + var joinTablesUsed: seq[string] + if joinargs != [] and $(joinargs.repr) != "[]": + for i, d in joinargs: + if d.repr.len == 0:# or joinargs.len() == 2: + continue + + if $d.table in joinTablesUsed: + continue + joinTablesUsed.add($d.table) + + if $d.tableAs != "" and $d.tableAs != $d.table: + tablesInQuery.add(($d.table, $d.tableAs)) + else: + tablesInQuery.add(($d.table, "")) + + # + # Base - from table # - # Base let tableName = if ($tableAs).len() > 0 and $table != $tableAs: $table & " AS " & $tableAs @@ -149,13 +303,223 @@ macro sqlSelectConst*( $table + # + # Select + if select.len() == 0: + raise newException( + Exception, + "Bad SQL format. Please check your SQL statement. " & + "This is most likely caused by a missing SELECT clause. " & + "Bug: `select.len() == 0` in \n" & $select + ) + var res = sqlSelectConstSelect(select) + # var res = "SELECT " + + # for i, d in select: + # if i > 0: res.add(", ") + # res.add($d) + + + # + # Joins + # + var lef = sqlSelectConstJoin( + joinargs, jointype) + # deleteMarkersFields, hideIsDeleted, deleteMarker) + # var lef = "" + + # for d in joinargs: + # if d.repr.len == 0 and joinargs.len() == 2: + # break + + # lef.add(" " & $jointype & " JOIN ") + # lef.add($d.table & " ") + + # if d.tableAs != "" and d.tableAs != d.table: + # lef.add("AS " & $d.tableAs & " ") + + # lef.add("ON (") + + # for i, join in d.on: + # if i > 0: + # lef.add(" AND ") + # lef.add($join) + + # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + # var hit = false + # for t in tablesWithDeleteMarker: + # if $t == $d.table: + # hit = true + # break + + # if hit: + # lef.add( + # " AND " & + # ( + # if d.tableAs.len() > 0 and $d.tableAs != $d.table: + # $d.tableAs + # else: + # $d.table + # ) & + # $deleteMarker + # ) + + # lef.add(")") + + + # + # Where - normal + # + var wes = sqlSelectConstWhere(where) + # var wes = "" + + # for i, d in where: + # let v = $d + + # if v.len() > 0 and i == 0: + # wes.add(" WHERE ") + + # if i > 0: + # wes.add(" AND ") + + # if v.len() > 0: + + # # => ... = NULL + # if v[(v.high - 3)..v.high] == "NULL": + # wes.add(v) + + # # => ? = ANY(...) + # elif v.len() > 5 and v[0..4] == "= ANY": + # wes.add("? " & v) + + # # => ... IN (?) + # elif v[(v.high - 2)..v.high] == " IN": + # wes.add(v & " (?)") + + # # => ? IN (...) + # elif v.len() > 2 and v[0..1] == "IN": + # wes.add("? " & v) + + # # => ... = ? + # else: + # wes.add(v & " ?") + + + + # + # Where - n IN (x,c,v) + # + var acc = "" + + acc.add sqlSelectConstWhereIn(wes, acc, whereInField, whereInValue) + + # if ($whereInField).len() > 0 and (whereInValue).len() > 0: + # if wes.len == 0: + # acc.add(" WHERE " & $whereInField & " in (") + # else: + # acc.add(" AND " & $whereInField & " in (") + + + # var inVal: string + + # for a in whereInValue: + # if inVal != "": + # inVal.add(",") + # inVal.add($a) + + # acc.add(if inVal == "": "0" else: inVal) + # acc.add(")") + + + + # + # Soft delete + # + var (toWes, toAcc) = sqlSelectConstSoft( + wes, acc, + # table, tableAs, + tablesInQuery, + deleteMarkersFields, + hideIsDeleted, deleteMarker + ) + wes.add(toWes) + acc.add(toAcc) + # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + # var hit = false + # for t in tablesWithDeleteMarker: + # if $t == $table: + # hit = true + # break + + # if hit: + # let tableNameToUse = + # if ($tableAs).len() > 0 and $tableAs != $table: + # $tableAs + # else: + # $table + + # if wes == "" and acc == "": + # wes.add(" WHERE " & $tableNameToUse & $deleteMarker) + # elif acc != "": + # acc.add(" AND " & $tableNameToUse & $deleteMarker) + # else: + # wes.add(" AND " & $tableNameToUse & $deleteMarker) + + + # + # Combine the pretty SQL + # + let finalSQL = res & " FROM " & tableName & lef & wes & acc & " " & $customSQL + + + # + # Error checking + # + # => Bad format + const illegalFormats = [ + "WHERE AND", + "WHERE OR", + "AND AND", + "OR OR", + "AND OR", + "OR AND", + "WHERE IN", + "WHERE =", + "WHERE >", + "WHERE <", + "WHERE !", + "WHERE LIKE", + "WHERE NOT", + "WHERE IS", + "WHERE NULL", + "WHERE ANY" + ] + + for f in illegalFormats: + if f in finalSQL: + raise newException( + Exception, + "Bad SQL format. Please check your SQL statement. " & + "This is most likely caused by a missing WHERE clause. " & + "Bug: `" & f & "` in \n" & finalSQL + ) + + if $table != $tableAs and lef.len() > 0: + var hit: bool + for s in select: + if "." notin s: + echo "WARNING: Missing table alias in select statement: " & $s + hit = true + if hit: + echo "WARNING: " & finalSQL when defined(verboseSqlquery): echo "SQL Macro:" - echo res & " FROM " & tableName & lef & wes & acc & " " & $customSQL + echo finalSQL - result = parseStmt("sql(\"" & res & " FROM " & tableName & lef & wes & acc & " " & $customSQL & "\")") + result = parseStmt("sql(\"" & finalSQL & "\")") + @@ -171,7 +535,7 @@ proc sqlSelect*( joinoverride: string = "", # WHERE-IN whereInField: string = "", - whereInValue: seq[string] = @[], + whereInValue: seq[string] = @[], # Could be unsafe. Is not checked. whereInValueString: seq[string] = @[], whereInValueInt: seq[int] = @[], # Custom SQL, e.g. ORDER BY @@ -182,15 +546,74 @@ proc sqlSelect*( tableAs: string = table, # Soft delete hideIsDeleted: bool = true, - tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + tablesWithDeleteMarker: varargs[string] = [], #(when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], deleteMarker = ".is_deleted IS NULL", ): SqlQuery = ## SQL builder for SELECT queries + # + # Set delete fields + # + # 1) macro options + # 2) default options set outside macro + # + var deleteMarkersFields: seq[string] + for t in tablesWithDeleteMarker: + # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: + # continue + if t == "": + continue + if t notin deleteMarkersFields: + deleteMarkersFields.add(t) + + when declared(tablesWithDeleteMarkerInit): + for t in tablesWithDeleteMarkerInit: + if t == "": + continue + # if t.repr.len == 0 or tablesWithDeleteMarkerInit.len() == 2: + # continue + if t notin deleteMarkersFields: + deleteMarkersFields.add(t) + + # + # Create seq of tables + # + var tablesInQuery: seq[tuple[table: string, tableAs: string]] + + # Base table + if $tableAs != "" and $table != $tableAs: + tablesInQuery.add(($table, $tableAs)) + else: + tablesInQuery.add(($table, "")) + + # Join table + for d in joinargs: + # if d.repr.len == 0 and joinargs.len() == 2: + # continue + if d.table == "": + continue + if d.tableAs != "" and d.tableAs != d.table: + tablesInQuery.add((d.table, d.tableAs)) + else: + tablesInQuery.add((d.table, "")) + + + + # + # Base - from table + # + let tableName = + if tableAs != "" and table != tableAs: + table & " AS " & tableAs + else: + table + # # Select + # var res = "SELECT " + for i, d in select: if i > 0: res.add(", ") res.add(d) @@ -198,7 +621,9 @@ proc sqlSelect*( # # Joins + # var lef = "" + for i, d in joinargs: lef.add(" " & $jointype & " JOIN ") lef.add(d.table & " ") @@ -207,20 +632,32 @@ proc sqlSelect*( lef.add("AS " & d.tableAs & " ") lef.add("ON (") + for i, join in d.on: if i > 0: lef.add(" AND ") lef.add(join) - if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and d.table in tablesWithDeleteMarker: - lef.add(" AND " & (if d.tableAs != "" and d.tableAs != d.table: d.tableAs else: d.table) & deleteMarker) + # if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and d.table in tablesWithDeleteMarker: + # lef.add( + # " AND " & + # ( + # if d.tableAs != "" and d.tableAs != d.table: + # d.tableAs + # else: + # d.table + # ) & + # deleteMarker + # ) lef.add(")") if joinoverride.len() > 0: lef.add(" " & joinoverride) + + # + # Where - normal # - # Where var wes = "" for i, d in where: if d != "" and i == 0: @@ -230,15 +667,36 @@ proc sqlSelect*( wes.add(" AND ") if d != "": + # => ... = NULL if checkedArgs.len() > 0 and checkedArgs[i].isNull: wes.add(d & " NULL") + + # => ... = NULL + elif d[(d.high - 3)..d.high] == "NULL": + wes.add(d) + + # => ? = ANY(...) + elif d.len() > 5 and d[0..4] == "= ANY": + wes.add("? " & d) + + # => ... IN (?) + elif d[(d.high - 2)..d.high] == " IN": + wes.add(d & " (?)") + + # => ? IN (...) + elif d.len() > 2 and d[0..1] == "IN": + wes.add("? " & d) + + # => ... = ? else: wes.add(d & " ?") # # Where IN + # var acc = "" + if whereInField != "" and (whereInValue.len() > 0 or whereInValueString.len() > 0 or whereInValueInt.len() > 0): if wes.len == 0: acc.add(" WHERE " & whereInField & " in (") @@ -258,7 +716,7 @@ proc sqlSelect*( if inVal != "": inVal.add(",") - inVal.add("'" & a & "'") + inVal.add("'" & dbQuotePrivate(a) & "'") else: for a in whereInValueInt: @@ -266,7 +724,16 @@ proc sqlSelect*( inVal.add(",") inVal.add($a) - acc.add(if inVal == "": "0" else: inVal) + if inVal.len() == 0: + if whereInValue.len() > 0: + acc.add("0") + elif whereInValueString.len() > 0: + acc.add("''") + elif whereInValueInt.len() > 0: + acc.add("0") + else: + acc.add(inVal) + acc.add(")") @@ -274,28 +741,39 @@ proc sqlSelect*( # # Soft delete - if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and table in tablesWithDeleteMarker: - let tableNameToUse = - if tableAs.len() > 0 and tableAs != table: - tableAs - else: - table + # + if hideIsDeleted and deleteMarkersFields.len() > 0: + for t in tablesInQuery: + if t.table notin deleteMarkersFields: + continue + + let toUse = if t.tableAs != "": t.tableAs else: t.table + + if wes == "" and acc == "": + wes.add(" WHERE " & toUse & $deleteMarker) + + elif acc != "": + acc.add(" AND " & toUse & $deleteMarker) + + else: + wes.add(" AND " & toUse & $deleteMarker) + + + # let tableNameToUse = + # if tableAs.len() > 0 and tableAs != table: + # tableAs + # else: + # table + + # if wes == "" and acc == "": + # wes.add(" WHERE " & tableNameToUse & deleteMarker) + # elif acc != "": + # acc.add(" AND " & tableNameToUse & deleteMarker) + # else: + # wes.add(" AND " & tableNameToUse & deleteMarker) - if wes == "" and acc == "": - wes.add(" WHERE " & tableNameToUse & deleteMarker) - elif acc != "": - acc.add(" AND " & tableNameToUse & deleteMarker) - else: - wes.add(" AND " & tableNameToUse & deleteMarker) - # - # Alias - let tableName = - if tableAs != "" and table != tableAs: - table & " AS " & tableAs - else: - table # @@ -310,15 +788,24 @@ proc sqlSelect*( - - +# +# Legacy +# proc sqlSelect*( - table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, + table: string, + data: varargs[string], + left: varargs[string], + whereC: varargs[string], + access: string, accessC: string, + user: string, args: ArgsContainer.query = @[], hideIsDeleted: bool = true, - tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + tablesWithDeleteMarker: varargs[string] = [], #(when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], deleteMarker = ".is_deleted IS NULL", ): SqlQuery {.deprecated.} = + ## + ## Legacy converter + ## var leftcon: seq[tuple[table: string, tableAs: string, on: seq[string]]] @@ -377,112 +864,13 @@ proc sqlSelect*( ) -#[ -proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = - ## SQL builder for SELECT queries - ## Does NOT check for NULL values - - var res = "SELECT " - for i, d in data: - if i > 0: res.add(", ") - res.add(d) - - var lef = "" - for i, d in left: - if d != "": - lef.add(" LEFT JOIN ") - lef.add(d) - - var wes = "" - for i, d in whereC: - if d != "" and i == 0: - wes.add(" WHERE ") - if i > 0: - wes.add(" AND ") - if d != "": - wes.add(d & " ?") - - var acc = "" - if access != "": - if wes.len == 0: - acc.add(" WHERE " & accessC & " in ") - acc.add("(") - else: - acc.add(" AND " & accessC & " in (") - var inVal: string - for a in split(access, ","): - if a == "": continue - if inVal != "": - inVal.add(",") - inVal.add(a) - acc.add(if inVal == "": "0" else: inVal) - acc.add(")") - - when defined(testSqlquery): - echo res & " FROM " & table & lef & wes & acc & " " & user - - when defined(test): - testout = res & " FROM " & table & lef & wes & acc & " " & user - - result = sql(res & " FROM " & table & lef & wes & acc & " " & user) - - -proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, args: ArgsContainer.query): SqlQuery = - ## SQL builder for SELECT queries - ## Checks for NULL values - - var res = "SELECT " - for i, d in data: - if i > 0: res.add(", ") - res.add(d) - - var lef = "" - for i, d in left: - if d != "": - lef.add(" LEFT JOIN ") - lef.add(d) - - var wes = "" - for i, d in whereC: - if d != "" and i == 0: - wes.add(" WHERE ") - if i > 0: - wes.add(" AND ") - if d != "": - if args[i].isNull: - wes.add(d & " = NULL") - else: - wes.add(d & " ?") - - var acc = "" - if access != "": - if wes.len == 0: - acc.add(" WHERE " & accessC & " in ") - acc.add("(") - else: - acc.add(" AND " & accessC & " in (") - - var inVal: string - for a in split(access, ","): - if a == "": continue - if inVal != "": - inVal.add(",") - inVal.add(a) - acc.add(if inVal == "": "0" else: inVal) - acc.add(")") - - when defined(testSqlquery): - echo res & " FROM " & table & lef & wes & acc & " " & user - - when defined(test): - testout = res & " FROM " & table & lef & wes & acc & " " & user - - result = sql(res & " FROM " & table & lef & wes & acc & " " & user) -]# macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery {.deprecated.} = ## SQL builder for SELECT queries ## Does NOT check for NULL values + ## + ## Legacy converter + ## var res: string for i, d in data: diff --git a/src/sqlbuilderpkg/select_legacy.nim b/src/sqlbuilderpkg/select_legacy.nim index da764c1..c57cf41 100644 --- a/src/sqlbuilderpkg/select_legacy.nim +++ b/src/sqlbuilderpkg/select_legacy.nim @@ -1,8 +1,13 @@ # Copyright 2020 - Thomas T. Jarløv +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common import - std/db_common, std/macros, std/strutils @@ -19,7 +24,7 @@ proc sqlSelect*( table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, args: ArgsContainer.query = @[], hideIsDeleted: bool = true, - tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarker): tablesWithDeleteMarker else: []), #@[], + tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], deleteMarker = ".is_deleted IS NULL", ): SqlQuery {.deprecated.} = ## diff --git a/src/sqlbuilderpkg/totypes.nim b/src/sqlbuilderpkg/totypes.nim new file mode 100644 index 0000000..76e65ce --- /dev/null +++ b/src/sqlbuilderpkg/totypes.nim @@ -0,0 +1,66 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +import + std/strutils + + +# proc toSnakeCase(s: string): string = +# for c in s: +# if c.isUpperAscii(): +# if len(result) > 0: +# result.add('_') +# result.add(c.toLowerAscii()) +# else: +# result.add(c) + +proc parseVal[T: bool](data: string, v: var T) = + v = (data == "t" or data == "true") + +proc parseFloat[T: float](data: string, v: var T) = + if data == "": + v = 0.0 + else: + v = data.parseFloat() + +proc parseVal[T: int](data: string, v: var T) = + if data == "": + v = 0 + else: + v = data.parseInt() + +proc parseVal[T: string](data: string, v: var T) = + v = data + + +proc parseVal[T: bool](v: var T) = (v = false) + +proc parseVal[T: float](v: var T) = (v = 0.0) + +proc parseVal[T: int](v: var T) = (v = 0) + +proc parseVal[T: string](v: var T) = (v = "") + + +proc sqlToType*[T](t: typedesc[T], columns, val: seq[string]): T = + let tmp = t() + + for fieldName, field in tmp[].fieldPairs: + var + found = false + for ci in 0..columns.high: + if columns[ci] == fieldName:#.toSnakeCase: + parseVal(val[ci], field) + found = true + break + if not found: + parseVal(field) + else: + found = false + + return tmp + + +proc sqlToType*[T](t: typedesc[T], columns: seq[string], vals: seq[seq[string]]): seq[T] = + for v in vals: + result.add(sqlToType(t, columns, v)) + return result \ No newline at end of file diff --git a/src/sqlbuilderpkg/update.nim b/src/sqlbuilderpkg/update.nim index 0be7e8e..f461282 100644 --- a/src/sqlbuilderpkg/update.nim +++ b/src/sqlbuilderpkg/update.nim @@ -1,8 +1,13 @@ # Copyright 2020 - Thomas T. Jarløv +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common import - std/db_common, std/macros, std/strutils @@ -10,6 +15,106 @@ import ./utils +proc updateSetFormat(v: string): string = + let + field = v.strip() + fieldSplit = field.split("=") + + # + # Does the data have a `=` sign? + # + if fieldSplit.len() == 2: + # + # If the data is only having equal but no value, insert a `?` sign + # + if fieldSplit[1] == "": + return (field & " ?") + # + # Otherwise just add the data as is, eg. `field = value` + # + else: + return (field) + + # + # Otherwise revert to default + # + else: + return (field & " = ?") + + + +proc updateSet(data: varargs[string]): string = + for i, d in data: + if i > 0: + result.add(", ") + + result.add(updateSetFormat(d)) + + return result + + +proc updateWhereFormat(v: string): string = + let + field = v.strip() + + var fieldSplit: seq[string] + if field.contains(" "): + if field.contains("="): + fieldSplit = field.split("=") + elif field.contains("IS NOT"): + fieldSplit = field.split("IS NOT") + elif field.contains("IS"): + fieldSplit = field.split("IS") + elif field.contains("NOT IN"): + fieldSplit = field.split("NOT IN") + elif field.contains("IN"): + fieldSplit = field.split("IN") + elif field.contains("!="): + fieldSplit = field.split("!=") + elif field.contains("<="): + fieldSplit = field.split("<=") + elif field.contains(">="): + fieldSplit = field.split(">=") + elif field.contains("<"): + fieldSplit = field.split("<") + elif field.contains(">"): + fieldSplit = field.split(">") + else: + fieldSplit = field.split("=") + + # + # Does the data have a `=` sign? + # + if fieldSplit.len() == 2: + # + # If the data is only having equal but no value, insert a `?` sign + # + if fieldSplit[1] == "": + return (field & " ?") + # + # Otherwise just add the data as is, eg. `field = value` + # + else: + return (field) + + # + # Otherwise revert to default + # + else: + return (field & " = ?") + + +proc updateWhere(where: varargs[string]): string = + var wes = " WHERE " + for i, v in where: + if i > 0: + wes.add(" AND ") + + wes.add(updateWhereFormat(v)) + + return wes + + proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], args: ArgsContainer.query): SqlQuery = ## SQL builder for UPDATE queries ## Checks for NULL values @@ -20,6 +125,10 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], ar fields.add(", ") if args[i].isNull: fields.add(d & " = NULL") + elif d.len() > 5 and d[(d.high-3)..d.high] == "NULL": + fields.add(d) + elif d[d.high-1..d.high] == "=": + fields.add(d & " ?") else: fields.add(d & " = ?") @@ -27,7 +136,10 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], ar for i, d in where: if i > 0: wes.add(" AND ") - wes.add(d & " = ?") + if d[d.high..d.high] == "=": + wes.add(d & " ?") + else: + wes.add(d & " = ?") when defined(testSqlquery): echo fields & wes @@ -38,29 +150,57 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], ar result = sql(fields & wes) -proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string]): SqlQuery = +proc sqlUpdate*( + table: string, + data: varargs[string], + where: varargs[string] + ): SqlQuery = ## SQL builder for UPDATE queries ## Does NOT check for NULL values var fields = "UPDATE " & table & " SET " - for i, d in data: + + fields.add(updateSet(data)) + + fields.add(updateWhere(where)) + + + when defined(testSqlquery): + echo fields + + when defined(test): + testout = fields + + result = sql(fields) + + + + +proc updateSet(data: NimNode): string = + for i, v in data: + # Convert NimNode to string + let d = $v + if i > 0: - fields.add(", ") - fields.add(d & " = ?") + result.add(", ") + + result.add(updateSetFormat(d)) + return result + + +proc updateWhere(where: NimNode): string = var wes = " WHERE " - for i, d in where: + for i, v in where: + # Convert NimNode to string + let d = $v + if i > 0: wes.add(" AND ") - wes.add(d & " = ?") - when defined(testSqlquery): - echo fields & wes + wes.add(updateWhereFormat(d)) - when defined(test): - testout = fields & wes - - result = sql(fields & wes) + return wes macro sqlUpdateMacro*(table: string, data: varargs[string], where: varargs[string]): SqlQuery = @@ -68,17 +208,12 @@ macro sqlUpdateMacro*(table: string, data: varargs[string], where: varargs[strin ## Does NOT check for NULL values var fields = "UPDATE " & $table & " SET " - for i, d in data: - if i > 0: - fields.add(", ") - fields.add($d & " = ?") - var wes = " WHERE " - for i, d in where: - if i > 0: - wes.add(" AND ") - wes.add($d & " = ?") + + fields.add(updateSet(data)) + + fields.add(updateWhere(where)) when defined(testSqlquery): - echo fields & wes + echo fields - result = parseStmt("sql(\"" & fields & wes & "\")") \ No newline at end of file + result = parseStmt("sql(\"" & fields & "\")") \ No newline at end of file diff --git a/src/sqlbuilderpkg/utils.nim b/src/sqlbuilderpkg/utils.nim index b71aa7c..6edc40a 100644 --- a/src/sqlbuilderpkg/utils.nim +++ b/src/sqlbuilderpkg/utils.nim @@ -161,4 +161,4 @@ template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq else: argsContainer.query.add(argObject) argsContainer.args.add(argObject.val) - (select, argsContainer) \ No newline at end of file + (select, argsContainer) diff --git a/src/sqlbuilderpkg/utils_private.nim b/src/sqlbuilderpkg/utils_private.nim new file mode 100644 index 0000000..ccf0505 --- /dev/null +++ b/src/sqlbuilderpkg/utils_private.nim @@ -0,0 +1,13 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + + + +proc dbQuotePrivate*(s: string): string = + ## DB quotes the string. + result = "'" + for c in items(s): + case c + of '\'': add(result, "''") + of '\0': add(result, "\\0") + else: add(result, c) + add(result, '\'') \ No newline at end of file From bb1381eb0312d102cc98e2747e6e2f9f52da11d1 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 20:00:11 +0200 Subject: [PATCH 04/27] Add tests --- tests/test.nim | 581 ---------------------- tests/test2.nim | 88 ++-- tests/test_args.nim | 76 +++ tests/test_insert.nim | 51 ++ tests/test_legacy.nim | 212 ++++++++ tests/test_legacy_with_softdelete.nim | 155 ++++++ tests/test_legacy_with_softdelete2.nim | 213 +++++++++ tests/test_result_to_types.nim | 118 +++++ tests/test_select.nim | 360 ++++++++++++++ tests/test_select_const.nim | 639 +++++++++++++++++++++++++ tests/test_update.nim | 207 ++++++++ 11 files changed, 2058 insertions(+), 642 deletions(-) delete mode 100644 tests/test.nim create mode 100644 tests/test_args.nim create mode 100644 tests/test_insert.nim create mode 100644 tests/test_legacy.nim create mode 100644 tests/test_legacy_with_softdelete.nim create mode 100644 tests/test_legacy_with_softdelete2.nim create mode 100644 tests/test_result_to_types.nim create mode 100644 tests/test_select.nim create mode 100644 tests/test_select_const.nim create mode 100644 tests/test_update.nim diff --git a/tests/test.nim b/tests/test.nim deleted file mode 100644 index 28df9d2..0000000 --- a/tests/test.nim +++ /dev/null @@ -1,581 +0,0 @@ -# Copyright Thomas T. Jarløv (TTJ) - -import - std/db_common, - std/strutils, - std/unittest, - src/sqlbuilder - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - -suite "test formats": - - test "genArgsColumns": - let (s, a) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - - assert s == ["name", "age"] - - for k, v in a.query: - if k == 0: - assert $v == """(val: "", isNull: false)""" - if k == 1: - assert $v == """(val: "30", isNull: false)""" - if k == 3: - assert $v == """(val: "154", isNull: false)""" - - for k, v in a.args: - if k == 0: - assert $v == "" - if k == 1: - assert $v == "30" - if k == 3: - assert $v == "154" - - let a1 = sqlInsert("my-table", s, a.query) - let a2 = sqlDelete("my-table", s, a.query) - let a3 = sqlUpdate("my-table", s, ["id"], a.query) - let a4 = sqlSelect("my-table", s, [""], ["id ="], "", "", "", a.query) - - - test "genArgsSetNull": - let b = genArgsSetNull("hje", "", "12") - assert b.args == @["hje", "12"] - assert b.query.len() == 3 - for k, v in b.query: - if k == 0: - assert $v == """(val: "hje", isNull: false)""" - if k == 1: - assert $v == """(val: "", isNull: true)""" - if k == 2: - assert $v == """(val: "12", isNull: false)""" - - test "genArgs": - let b = genArgs("hje", "", "12") - assert b.args == @["hje", "", "12"] - assert b.query.len() == 3 - for k, v in b.query: - if k == 0: - assert $v == """(val: "hje", isNull: false)""" - if k == 1: - assert $v == """(val: "", isNull: false)""" - if k == 2: - assert $v == """(val: "12", isNull: false)""" - - test "genArgs with null": - let b = genArgs("hje", dbNullVal, "12") - assert b.args == @["hje", "12"] - assert b.query.len() == 3 - for k, v in b.query: - if k == 0: - assert $v == """(val: "hje", isNull: false)""" - if k == 1: - assert $v == """(val: "", isNull: true)""" - if k == 2: - assert $v == """(val: "12", isNull: false)""" - - test "sqlInsert": - let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - discard sqlInsert("my-table", s, a1.query) - assert testout == "INSERT INTO my-table (name, age) VALUES (?, ?)" - - let a2 = genArgsSetNull("hje", "") - discard sqlInsert("my-table", ["name", "age"], a2.query) - assert testout == "INSERT INTO my-table (name) VALUES (?)" - - test "sqlUpdate": - let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - discard sqlUpdate("my-table", s, ["id"], a1.query) - assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" - - let a2 = genArgsSetNull("hje", "") - discard sqlUpdate("my-table", ["name", "age"], ["id"], a2.query) - assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" - - let a3 = genArgs("hje", "") - discard sqlUpdate("my-table", ["name", "age"], ["id"], a3.query) - assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" - - let a4 = genArgs("hje", dbNullVal) - discard sqlUpdate("my-table", ["name", "age"], ["id"], a4.query) - assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" - - test "sqlSelect": - let a2 = genArgsSetNull("hje", "", "123") - let q1 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a2.query) - check querycompare(q1, sql"SELECT name, age FROM my-table WHERE id = ? ") - - let a3 = genArgs("hje", "") - let q2 = sqlSelect("my-table AS m", ["m.name", "m.age"], ["p ON p.id = m.id"], ["m.id ="], "", "", "", a3.query) - check querycompare(q2, sql"SELECT m.name, m.age FROM my-table AS m LEFT JOIN p ON (p.id = m.id) WHERE m.id = ? ") - - let a4 = genArgs("hje", dbNullVal) - let q3 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a4.query) - check querycompare(q3, sql"SELECT name, age FROM my-table WHERE id = ? ") - - -suite "test sqlSelect": - - test "sqlSelect - refactor 2022-01": - var test: SqlQuery - - test = sqlSelect( - table = "tasks", - select = @["id", "name", "description", "created", "updated", "completed"], - where = @["id ="], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) - - - test = sqlSelect( - table = "tasks", - tableAs = "t", - select = @["id", "name", "description", "created", "updated", "completed"], - where = @["id ="], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) - - - test = sqlSelect( - table = "tasks", - tableAs = "t", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) - - - test = sqlSelect( - table = "tasks", - tableAs = "t", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) - - - - test = sqlSelect( - table = "tasks", - tableAs = "t", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], - jointype = INNER, - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) - - - - let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] - - test = sqlSelect( - table = "tasks", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet - ) - check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) - - - - test = sqlSelect( - table = "tasks", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet - ) - check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid AND history.is_deleted IS NULL) WHERE id = ? AND tasks.is_deleted IS NULL ")) - - - - test = sqlSelect( - table = "tasks", - select = @["id", "name"], - where = @["id ="], - joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet, - deleteMarker = ".deleted_at = 543234563" - ) - check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid AND history.deleted_at = 543234563) WHERE id = ? AND tasks.deleted_at = 543234563 ")) - - - - test = sqlSelect( - table = "tasks", - select = @["tasks.id", "tasks.name"], - where = @["tasks.id ="], - joinargs = @[(table: "history", tableAs: "his", on: @["his.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet - ) - check querycompare(test, sql("SELECT tasks.id, tasks.name FROM tasks LEFT JOIN history AS his ON (his.id = tasks.hid AND his.is_deleted IS NULL) WHERE tasks.id = ? AND tasks.is_deleted IS NULL ")) - - - - test = sqlSelect( - table = "tasksitems", - tableAs = "tasks", - select = @[ - "tasks.id", - "tasks.name", - "tasks.status", - "tasks.created", - "his.id", - "his.name", - "his.status", - "his.created", - "projects.id", - "projects.name", - "person.id", - "person.name", - "person.email" - ], - where = @[ - "projects.id =", - "tasks.status >" - ], - joinargs = @[ - (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), - (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), - (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) - ], - whereInField = "tasks.id", - whereInValue = @["1", "2", "3"], - customSQL = "ORDER BY tasks.created DESC", - tablesWithDeleteMarker = tableWithDeleteMarkerLet - ) - check querycompare(test, (sql(""" - SELECT - tasks.id, - tasks.name, - tasks.status, - tasks.created, - his.id, - his.name, - his.status, - his.created, - projects.id, - projects.name, - person.id, - person.name, - person.email - FROM - tasksitems AS tasks - LEFT JOIN history AS his ON - (his.id = tasks.hid AND his.status = 1 AND his.is_deleted IS NULL) - LEFT JOIN projects ON - (projects.id = tasks.project_id AND projects.status = 1) - LEFT JOIN person ON - (person.id = tasks.person_id) - WHERE - projects.id = ? - AND tasks.status > ? - AND tasks.id in (1,2,3) - AND tasks.is_deleted IS NULL - ORDER BY - tasks.created DESC - """))) - - -suite "test sqlSelectMacro": - - test "sqlSelectMacro legacy - refactor 2022-01": - - let q1 = sqlSelectMacro( - table = "my-table", - data = ["name", "age"], - left = [""], - whereC = ["id ="], "", "", "") - - check querycompare(q1, sql("SELECT name, age FROM my-table WHERE id = ?")) - - -suite "test sqlSelectConst": - - test "sqlSelectConst - refactor 2022-01": - let a = sqlSelectConst( - table = "tasks", - tableAs = "t", - select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], - where = ["t.id ="], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker - ) - - check querycompare(a, (sql(""" - SELECT - t.id, t.name, t.description, t.created, t.updated, t.completed - FROM - tasks AS t - WHERE - t.id = ? - AND t.is_deleted IS NULL - """))) - - - let b = sqlSelectConst( - table = "tasks", - # tableAs = "t", - select = ["id", "name", "description", "created", "updated", "completed"], - where = ["id =", "status >"], - joinargs = [ - (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), - (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), - ], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] #tableWithDeleteMarker - ) - - check querycompare(b, sql(""" - SELECT - id, name, description, created, updated, completed - FROM - tasks - LEFT JOIN history AS his ON - (his.id = tasks.hid AND his.status = 1 AND his.is_deleted IS NULL) - LEFT JOIN projects ON - (projects.id = tasks.project_id AND projects.status = 1) - WHERE - id = ? - AND status > ? - AND tasks.is_deleted IS NULL - """)) - - - let c = sqlSelectConst( - table = "tasks", - select = ["tasks.id"], - where = ["status >"], - joinargs = [ - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - ], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] - ) - check querycompare(c, sql(""" - SELECT - tasks.id - FROM - tasks - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - WHERE - status > ? - AND tasks.is_deleted IS NULL - """)) - - - let d = sqlSelectConst( - table = "tasks", - select = ["tasks.id"], - where = ["status >"], - joinargs = [ - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), - ], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] - ) - check querycompare(d, sql(""" - SELECT - tasks.id - FROM - tasks - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - LEFT JOIN history ON - (his.id = tasks.hid AND history.is_deleted IS NULL) - WHERE - status > ? - AND tasks.is_deleted IS NULL - """)) - - - let e = sqlSelectConst( - table = "tasks", - tableAs = "t", - select = ["t.id", "t.name"], - where = ["t.id ="], - whereInField = "t.name", - whereInValue = ["'1aa'", "'2bb'", "'3cc'"], - tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker - ) - check querycompare(e, sql(""" - SELECT - t.id, t.name - FROM - tasks AS t - WHERE - t.id = ? - AND t.name in ('1aa','2bb','3cc')""")) - - - let f = sqlSelectConst( - table = "tasks", - tableAs = "t", - select = ["t.id", "t.name"], - where = ["t.id ="], - whereInField = "t.id", - whereInValue = [""], - tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker - ) - check querycompare(f, sql("SELECT t.id, t.name FROM tasks AS t WHERE t.id = ? AND t.id in (0)")) - - -suite "test sqlSelect(Convert) - legacy": - - test "sqlSelectConvert - refactor 2022-01": - - var test: SqlQuery - - - test = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "") - - check querycompare(test, sql("SELECT name, age FROM my-table WHERE id = ?")) - - - - test = sqlSelect("tasks", ["tasks.id", "tasks.name"], ["project ON project.id = tasks.project_id"], ["id ="], "", "", "") - - check querycompare(test, sql(""" - SELECT - tasks.id, - tasks.name - FROM - tasks - LEFT JOIN project ON - (project.id = tasks.project_id) - WHERE - id = ? - """)) - - - - test = sqlSelect("tasks", ["tasks.id", "tasks.name", "p.id"], ["project AS p ON p.id = tasks.project_id"], ["tasks.id ="], "", "", "") - - check querycompare(test, sql(""" - SELECT - tasks.id, - tasks.name, - p.id - FROM - tasks - LEFT JOIN project AS p ON - (p.id = tasks.project_id) - WHERE - tasks.id = ? - """)) - - - - test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "", "", "") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN project AS p ON - (p.id = t.project_id) - WHERE - t.id = ? - """)) - - - - test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN project AS p ON - (p.id = t.project_id) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - ORDER BY - t.name - """)) - - - - test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name", tablesWithDeleteMarker = ["tasks", "persons"]) - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN project AS p ON - (p.id = t.project_id) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - AND t.is_deleted IS NULL - ORDER BY - t.name - """)) - - - test "sqlSelectConvert with genArgs - refactor 2022-01": - - - var a = genArgs("123", dbNullVal) - - var test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS"], "", "", "", a.query) - - check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NULL""")) - - - - a = genArgs("123", dbNullVal, dbNullVal) - - test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) - - check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) - - - test "sqlSelectConvert with genArgsSetNull - refactor 2022-01": - - var a = genArgsSetNull("123", "", "") - - var test = sqlSelect("tasks", ["tasks.id", "tasks.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) - - check querycompare(test, sql("""SELECT tasks.id, tasks.name FROM tasks WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) \ No newline at end of file diff --git a/tests/test2.nim b/tests/test2.nim index eb52414..ff95e85 100644 --- a/tests/test2.nim +++ b/tests/test2.nim @@ -2,24 +2,24 @@ -import src/sqlbuilderpkg/insert -export insert +# import src/sqlbuilderpkg/insert +# export insert -import src/sqlbuilderpkg/update -export update +# import src/sqlbuilderpkg/update +# export update -import src/sqlbuilderpkg/delete -export delete +# import src/sqlbuilderpkg/delete +# export delete -import src/sqlbuilderpkg/utils -export utils +# import src/sqlbuilderpkg/utils +# export utils -const tablesWithDeleteMarker = ["tasks", "persons"] +import std/unittest +const tablesWithDeleteMarkerInit = ["tasks", "persons"] -include src/sqlbuilderpkg/select +include src/sqlbuilder_include -import std/unittest proc querycompare(a, b: SqlQuery): bool = @@ -54,65 +54,31 @@ suite "test sqlSelect": check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) -suite "test sqlSelectConst": + +suite "test sqlSelect with inline NULL": test "set tablesWithDeleteMarker": - let a = sqlSelectConst( + let test = sqlSelect( table = "tasks", - tableAs = "t", - select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], - where = ["t.id ="], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name = NULL"], + hideIsDeleted = false ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND name = NULL ")) -suite "test sqlSelect(converter) legacy": +suite "test sqlSelectConst": test "set tablesWithDeleteMarker": + let a = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + # joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + where = ["t.id ="], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker + ) - let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN project AS p ON - (p.id = t.project_id) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - AND t.is_deleted IS NULL - ORDER BY - t.name - """)) - - - test "double delete": - - - let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN project AS p ON - (p.id = t.project_id) - LEFT JOIN persons ON - (persons.id = tasks.person_id AND persons.is_deleted IS NULL AND persons.is_deleted IS NULL) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - AND t.is_deleted IS NULL - ORDER BY - t.name - """)) \ No newline at end of file diff --git a/tests/test_args.nim b/tests/test_args.nim new file mode 100644 index 0000000..96760f9 --- /dev/null +++ b/tests/test_args.nim @@ -0,0 +1,76 @@ +# Copyright Thomas T. Jarløv (TTJ) + +import + std/unittest + +import + src/sqlbuilder + + + + + +suite "test formats": + + test "genArgsColumns": + let (s, a) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + + assert s == ["name", "age"] + + for k, v in a.query: + if k == 0: + assert $v == """(val: "", isNull: false)""" + if k == 1: + assert $v == """(val: "30", isNull: false)""" + if k == 3: + assert $v == """(val: "154", isNull: false)""" + + for k, v in a.args: + if k == 0: + assert $v == "" + if k == 1: + assert $v == "30" + if k == 3: + assert $v == "154" + + let a1 = sqlInsert("my-table", s, a.query) + let a2 = sqlDelete("my-table", s, a.query) + let a3 = sqlUpdate("my-table", s, ["id"], a.query) + let a4 = sqlSelect("my-table", s, [""], ["id ="], "", "", "", a.query) + + + test "genArgsSetNull": + let b = genArgsSetNull("hje", "", "12") + assert b.args == @["hje", "12"] + assert b.query.len() == 3 + for k, v in b.query: + if k == 0: + assert $v == """(val: "hje", isNull: false)""" + if k == 1: + assert $v == """(val: "", isNull: true)""" + if k == 2: + assert $v == """(val: "12", isNull: false)""" + + test "genArgs": + let b = genArgs("hje", "", "12") + assert b.args == @["hje", "", "12"] + assert b.query.len() == 3 + for k, v in b.query: + if k == 0: + assert $v == """(val: "hje", isNull: false)""" + if k == 1: + assert $v == """(val: "", isNull: false)""" + if k == 2: + assert $v == """(val: "12", isNull: false)""" + + test "genArgs with null": + let b = genArgs("hje", dbNullVal, "12") + assert b.args == @["hje", "12"] + assert b.query.len() == 3 + for k, v in b.query: + if k == 0: + assert $v == """(val: "hje", isNull: false)""" + if k == 1: + assert $v == """(val: "", isNull: true)""" + if k == 2: + assert $v == """(val: "12", isNull: false)""" diff --git a/tests/test_insert.nim b/tests/test_insert.nim new file mode 100644 index 0000000..fc5b76b --- /dev/null +++ b/tests/test_insert.nim @@ -0,0 +1,51 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + +suite "insert": + + test "sqlInsert - dynamic columns": + var test: SqlQuery + + let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + test = sqlInsert("my-table", s, a1.query) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) + + + test "sqlInsert - setting null": + var test: SqlQuery + + let a2 = genArgsSetNull("hje", "") + test = sqlInsert("my-table", ["name", "age"], a2.query) + # discard tryInsertID(sqlInsert("my-table", ["name", "age"], a2.query), a2.args) + check querycompare(test, sql("INSERT INTO my-table (name) VALUES (?)")) \ No newline at end of file diff --git a/tests/test_legacy.nim b/tests/test_legacy.nim new file mode 100644 index 0000000..4e80476 --- /dev/null +++ b/tests/test_legacy.nim @@ -0,0 +1,212 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder + + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + + + + + +suite "legacy - sqlSelect(Convert)": + + + test "legacy - sqlSelect - simple": + let a2 = genArgsSetNull("hje", "", "123") + let q1 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a2.query) + check querycompare(q1, sql"SELECT name, age FROM my-table WHERE id = ? ") + + let a3 = genArgs("hje", "") + let q2 = sqlSelect("my-table AS m", ["m.name", "m.age"], ["p ON p.id = m.id"], ["m.id ="], "", "", "", a3.query) + check querycompare(q2, sql"SELECT m.name, m.age FROM my-table AS m LEFT JOIN p ON (p.id = m.id) WHERE m.id = ? ") + + let a4 = genArgs("hje", dbNullVal) + let q3 = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "", a4.query) + check querycompare(q3, sql"SELECT name, age FROM my-table WHERE id = ? ") + + + test "sqlSelect - #1": + var test: SqlQuery + + test = sqlSelect("my-table", ["name", "age"], [""], ["id ="], "", "", "") + + check querycompare(test, sql("SELECT name, age FROM my-table WHERE id = ?")) + + + test "sqlSelect - #2 - join": + var test: SqlQuery + + test = sqlSelect("tasksQQ", ["tasksQQ.id", "tasksQQ.name"], ["project ON project.id = tasksQQ.project_id"], ["id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + tasksQQ.id, + tasksQQ.name + FROM + tasksQQ + LEFT JOIN project ON + (project.id = tasksQQ.project_id) + WHERE + id = ? + """)) + + + test "sqlSelect - #3 - join with alias": + var test: SqlQuery + + test = sqlSelect("tasksQQ", ["tasksQQ.id", "tasksQQ.name", "p.id"], ["project AS p ON p.id = tasksQQ.project_id"], ["tasksQQ.id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + tasksQQ.id, + tasksQQ.name, + p.id + FROM + tasksQQ + LEFT JOIN project AS p ON + (p.id = tasksQQ.project_id) + WHERE + tasksQQ.id = ? + """)) + + + test "sqlSelect - #4 - alias all the way": + var test: SqlQuery + + test = sqlSelect("tasksQQ AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "", "", "") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasksQQ AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + """)) + + + test "sqlSelect - #5 - alias all the way with IN": + var test: SqlQuery + + test = sqlSelect("tasksQQ AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasksQQ AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + ORDER BY + t.name + """)) + + + test "sqlSelect - #6 - alias all the way with IN and delete marker": + var test: SqlQuery + + test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name", tablesWithDeleteMarker = ["tasks", "persons"]) + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + +suite "legacy - sqlSelect(Convert) - genArgs": + + test "sqlSelect with genArgs - refactor 2022-01": + + var a = genArgs("123", dbNullVal) + + var test = sqlSelect("tasksQQ", ["tasksQQ.id", "tasksQQ.name"], [""], ["id =", "status IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasksQQ.id, tasksQQ.name FROM tasksQQ WHERE id = ? AND status IS NULL""")) + + + + a = genArgs("123", dbNullVal, dbNullVal) + + test = sqlSelect("tasksQQ", ["tasksQQ.id", "tasksQQ.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasksQQ.id, tasksQQ.name FROM tasksQQ WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) + + + + test "sqlSelect with genArgsSetNull - refactor 2022-01": + + var a = genArgsSetNull("123", "", "") + + var test = sqlSelect("tasksQQ", ["tasksQQ.id", "tasksQQ.name"], [""], ["id =", "status IS NOT", "phase IS"], "", "", "", a.query) + + check querycompare(test, sql("""SELECT tasksQQ.id, tasksQQ.name FROM tasksQQ WHERE id = ? AND status IS NOT NULL AND phase IS NULL""")) + + + + +suite "test sqlSelectMacro": + + test "sqlSelectMacro legacy - refactor 2022-01": + + let q1 = sqlSelectMacro( + table = "my-table", + data = ["name", "age"], + left = [""], + whereC = ["id ="], "", "", "") + + check querycompare(q1, sql("SELECT name, age FROM my-table WHERE id = ?")) + + diff --git a/tests/test_legacy_with_softdelete.nim b/tests/test_legacy_with_softdelete.nim new file mode 100644 index 0000000..6731dea --- /dev/null +++ b/tests/test_legacy_with_softdelete.nim @@ -0,0 +1,155 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +# import +# src/sqlbuilder + +const tablesWithDeleteMarkerInit* = ["tasks", "history", "tasksitems"] +include + src/sqlbuilder_include + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + +suite "legacy - sqlSelect(converter) - with new functionality to avoid regression": + + + test "set tablesWithDeleteMarker": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "existing delete in where": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "AND tasks.is_deleted IS NULL ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL AND tasks.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "existing delete in where with alias": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "AND t.is_deleted IS NULL ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "existing delete in left join (double)": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id AND persons.is_deleted IS NULL) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "add delete marker in left join": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id", "persons ON persons.id = tasks.person_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN project AS p ON + (p.id = t.project_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + ORDER BY + t.name + """)) diff --git a/tests/test_legacy_with_softdelete2.nim b/tests/test_legacy_with_softdelete2.nim new file mode 100644 index 0000000..1584f69 --- /dev/null +++ b/tests/test_legacy_with_softdelete2.nim @@ -0,0 +1,213 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +# import +# src/sqlbuilder + +const tablesWithDeleteMarkerInit* = ["tasks", "history", "tasksitems", "persons", "actions", "project"] +include + src/sqlbuilder_include + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + +suite "legacy - sqlSelect(converter) - with new functionality to avoid regression - #2": + + + test "existing delete in left join (double) - delete marker from left join": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN invoice AS p ON + (p.id = t.invoice_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id AND persons.is_deleted IS NULL) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "add delete marker in left join - delete marker from left join": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN invoice AS p ON + (p.id = t.invoice_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + + + + + test "set left join without AS": + + let test = sqlSelect("tasks", ["t.id", "t.name", "invoice.id"], ["persons ON persons.id = t.persons_id"], ["t.id ="], "2,4,6,7", "invoice.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + invoice.id + FROM + tasks + LEFT JOIN persons ON + (persons.id = t.persons_id) + WHERE + t.id = ? + AND invoice.id in (2,4,6,7) + AND tasks.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + + + +#[ + test "set left join without AS": + + let test = sqlSelect("tasks", + [ + "DISTINCT actions.virtual_id", #0 + "actions.color", + "status_project.name", + "swimlanes_project.name", + "actions.name", + "phases_project.name", + "categories_project.name", + "actions.tags", + "actions.date_start", + "actions.date_end", + "actions.disp1", #10 + "actions.status", + "actions.modified", + "actions.description", + "actions.assigned_to", + "actions.dependenciesb", + "status_project.closed", + "(SELECT coalesce(string_agg(history.user_id::text, ','), '') FROM history WHERE history.item_id = actions.virtual_id AND history.project_id = actions.project_id AND (history.choice = 'Comment' OR history.choice = 'Picture' OR history.choice = 'File') AND history.is_deleted IS NULL) AS history", + "(SELECT COUNT(files_tasks.id) FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND (files_tasks.filetype IS NULL OR files_tasks.filetype != 1)) AS files", + "actions.floorplanJson", + "actions.canBeClosed", #20 + "personModified.uuid", + "actions.qa_type", + "personAssigned.uuid", + "personModified.name", + "cA.name", + "cA.logo", + "actions.assigned_to_company", + "actions.cx_review_document", + "actions.cx_review_page", + "actions.cx_bod", #30 + "actions.project_id", #31 + "actions.rand", #32 + "personAuthor.uuid", #33 + "personAuthor.name", #34 + "personAssigned.hasPicture", #35 + "personModified.hasPicture", #36 + "qap.virtual_id", #37 + "qap.name", #38 + "actions.cost", #39 + "personAuthor.hasPicture", #40 + "actions.creation", #41 + "floorplan.filename", #42 + "personAuthor.company", #43 + "(SELECT files_tasks.filename_hash FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND (files_tasks.filetype IS NULL OR files_tasks.filetype != 1) ORDER BY files_tasks.id DESC LIMIT 1) AS filesPhoto", + "(SELECT files_tasks.filename_hash FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND files_tasks.filetype = 1 ORDER BY files_tasks.id DESC LIMIT 1) AS filesFloorplan", + "(SELECT SUBSTRING(history.text, 0, 100) FROM history WHERE history.project_id = actions.project_id AND history.item_id = actions.virtual_id AND history.choice = 'Comment' AND history.is_deleted IS NULL ORDER BY history.id DESC LIMIT 1) AS lastComment", + "actions.location", #47 + "actions.customfields", #48 + "actions.testvalue", #49 + "actions.added_closed", #50 + #"personAuthor.uuid", # + ], + [ + "categories_project ON actions.category = categories_project.id", + "person as personAuthor ON actions.author_id = personAuthor.id", + "person as personModified ON actions.modifiedBy = personModified.id", + "person as personAssigned ON actions.assigned_to_userid = personAssigned.id", + "status_project ON actions.status = status_project.status AND actions.project_id = status_project.project_id AND actions.swimlane = status_project.swimlane_id", + "swimlanes_project on actions.swimlane = swimlanes_project.id", + "phases_project ON actions.phase = phases_project.id", + "project ON actions.project_id = project.id", + "company AS cA ON cA.id = actions.assigned_to_companyid", + "qa_paradigm AS qap ON qap.project_id = actions.project_id AND qap.id = actions.qa_id", + "files AS floorplan ON floorplan.project_id = actions.project_id AND floorplan.filename_hash = actions.floorplanhash" # This forces us to use DISTINCT on actions.id + ], ["actions.project_id ="], "", "", "ORDER BY status_project.closed ASC, actions.modified DESC LIMIT 500 OFFSET 0") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + project.id + FROM + tasks + LEFT JOIN persons ON + (persons.id = t.persons_id) + WHERE + t.id = ? + AND project.id in (2,4,6,7) + AND tasks.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + +]# \ No newline at end of file diff --git a/tests/test_result_to_types.nim b/tests/test_result_to_types.nim new file mode 100644 index 0000000..d2a58a9 --- /dev/null +++ b/tests/test_result_to_types.nim @@ -0,0 +1,118 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + + + +import std/unittest + +when NimMajor >= 2: + import + db_connector/db_sqlite +else: + import + std/db_sqlite + +import + std/strutils + +import + src/sqlbuilderpkg/select, + src/sqlbuilderpkg/totypes + + +type + Person = ref object + id: int + name: string + age: int + ident: string + is_nimmer: bool + +# +# Set up a test database +# +let db = open("tests/mytest.db", "", "", "") + +db.exec(sql"DROP TABLE IF EXISTS my_table") +db.exec(sql"""CREATE TABLE my_table ( + id INTEGER, + name VARCHAR(50) NOT NULL, + age INTEGER, + ident TEXT, + is_nimmer BOOLEAN + )""") + +db.exec(sql"INSERT INTO my_table (id, name) VALUES (0, ?)", "Jack") + +for i in 1..5: + db.exec(sql("INSERT INTO my_table (id, name, age, ident, is_nimmer) VALUES (?, ?, ?, ?, ?)"), $i, "Joe-" & $i, $i, "Nim", (if i <= 2: "true" else: "false")) + +for i in 6..10: + db.exec(sql("INSERT INTO my_table (id, name, age, ident) VALUES (?, ?, ?, ?)"), $i, "Cathrine-" & $i, $i, "Lag") + + + +# +# Start testing +# +suite "Map result to types": + + test "getRow() result map post call": + let + columns = @["id","name"] + val = db.getRow(sql("SELECT " & columns.join(",") & " FROM my_table WHERE id = 1")) + res = sqlToType(Person, columns, val) + + check res.id == 1 + check res.name == "Joe-1" + check res.age == 0 + check res.ident == "" + + + test "getRow() with mixed column order": + let + columns = @["name","id","ident"] + val = db.getRow(sql("SELECT " & columns.join(",") & " FROM my_table WHERE id = 1")) + res = sqlToType(Person, columns, val) + + check res.id == 1 + check res.name == "Joe-1" + check res.age == 0 + check res.ident == "Nim" + + + test "getAllRows in a seq[T]": + let + columns = @["id","ident","name", "age", "is_nimmer"] + vals = Person.sqlToType( + columns, + db.getAllRows(sql("SELECT " & columns.join(",") & " FROM my_table WHERE ident = 'Nim'")) + ) + + check vals.len == 5 + + check vals[0].id == 1 + check vals[0].name == "Joe-1" + + check vals[1].id == 2 + check vals[1].name == "Joe-2" + + check vals[1].isNimmer == true + check vals[3].isNimmer == false + + + test "getRow() with select()": + let + columns = @["name","id","ident"] + res = sqlToType(Person, columns, db.getRow( + sqlSelect( + table = "my_table", + tableAs = "t", + select = columns, + where = @["id ="], + ), 1) + ) + + check res.id == 1 + check res.name == "Joe-1" + check res.age == 0 + check res.ident == "Nim" \ No newline at end of file diff --git a/tests/test_select.nim b/tests/test_select.nim new file mode 100644 index 0000000..fe814f0 --- /dev/null +++ b/tests/test_select.nim @@ -0,0 +1,360 @@ +# Copyright Thomas T. Jarløv (TTJ) + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + + + + +suite "test sqlSelect": + + test "hideIsDeleted = false": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) + + + test "from using AS ": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = @["t.id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t WHERE t.id = ? ")) + + + test "WHERE statements: general": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name !=", "updated >", "completed IS", "description LIKE"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? ")) + check string(test).count("?") == 5 + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name !=", "updated >", "completed IS", "description LIKE"], + customSQL = "AND name != 'test' AND created > ? ", + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? AND name != 'test' AND created > ? ")) + check string(test).count("?") == 6 + + + + test "WHERE statements: = ANY(...)": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "ids_array"], + where = @["id =", "= ANY(ids_array)"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) ")) + check string(test).count("?") == 2 + + + + test "WHERE statements: x IN y": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "ids_array"], + where = @["id =", "IN (ids_array)"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? IN (ids_array) ")) + check string(test).count("?") == 2 + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "ids_array"], + where = @["id =", "id IN"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND id IN (?) ")) + check string(test).count("?") == 2 + + + + test "WHERE statements: `is NULL` ": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name != NULL", "description = NULL"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) + check string(test).count("?") == 1 + + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name !=", "description = NULL"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND description = NULL ")) + check string(test).count("?") == 2 + + + +suite "test sqlSelect - joins": + + test "LEFT JOIN using AS values": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) + + test "LEFT JOIN (default)": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + test "INNER JOIN": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + jointype = INNER, + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + +suite "test sqlSelect - deletemarkers / softdelete": + + + test "deletemarkers from seq": + var test: SqlQuery + let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.is_deleted IS NULL AND history.is_deleted IS NULL ")) + + + + test = sqlSelect( + table = "tasks", + select = @["tasks.id", "tasks.name"], + where = @["tasks.id ="], + joinargs = @[(table: "history", tableAs: "his", on: @["his.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT tasks.id, tasks.name FROM tasks LEFT JOIN history AS his ON (his.id = tasks.hid) WHERE tasks.id = ? AND tasks.is_deleted IS NULL AND his.is_deleted IS NULL ")) + + + + test "deletemarkers on the fly": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = @["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test "custom deletemarker override": + var test: SqlQuery + let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] + + test = sqlSelect( + table = "tasks", + select = @["id", "name"], + where = @["id ="], + joinargs = @[(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet, + deleteMarker = ".deleted_at = 543234563" + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.deleted_at = 543234563 AND history.deleted_at = 543234563 ")) + + + + test "complex query": + var test: SqlQuery + + let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] + + + test = sqlSelect( + table = "tasksitems", + tableAs = "tasks", + select = @[ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + where = @[ + "projects.id =", + "tasks.status >" + ], + joinargs = @[ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) + ], + whereInField = "tasks.id", + whereInValue = @["1", "2", "3"], + customSQL = "ORDER BY tasks.created DESC", + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + AND tasks.is_deleted IS NULL + AND his.is_deleted IS NULL + ORDER BY + tasks.created DESC + """))) + + + + + diff --git a/tests/test_select_const.nim b/tests/test_select_const.nim new file mode 100644 index 0000000..f7df106 --- /dev/null +++ b/tests/test_select_const.nim @@ -0,0 +1,639 @@ +# Copyright Thomas T. Jarløv (TTJ) + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + + +suite "test sqlSelectConst": + + test "hideIsDeleted = false": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) + + + + test "from using AS ": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id ="], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) + + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + hideIsDeleted = false, + customSQL = "ORDER BY t.created DESC" + ) + check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t WHERE t.id = ? ORDER BY t.created DESC ")) + + + + test "WHERE statements: general": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? ")) + check string(test).count("?") == 5 + + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], + customSQL = "AND name != 'test' AND created > ? ", + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? AND name != 'test' AND created > ? ")) + check string(test).count("?") == 6 + + + + test "WHERE statements: = ANY(...)": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "ids_array"], + where = ["id =", "= ANY(ids_array)"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) ")) + check string(test).count("?") == 2 + + + + test "WHERE statements: = ANY(...) multiple instances": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "ids_array"], + where = ["id =", "= ANY(ids_array)", "= ANY(user_array)", "= ANY(tasks_array)"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) AND ? = ANY(user_array) AND ? = ANY(tasks_array) ")) + check string(test).count("?") == 4 + + + + test "WHERE statements: x IN y": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "ids_array"], + where = ["id =", "IN (ids_array)"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? IN (ids_array) ")) + check string(test).count("?") == 2 + + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "ids_array"], + where = ["id =", "id IN"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND id IN (?) ")) + check string(test).count("?") == 2 + + + + test "WHERE statements: `is NULL` ": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "name != NULL", "description = NULL"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) + check string(test).count("?") == 1 + + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "name !=", "description = NULL"], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND description = NULL ")) + check string(test).count("?") == 2 + + + + +suite "test sqlSelectConst - joins": + + test "LEFT JOIN using AS values": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) + + test "LEFT JOIN (default) - 1 value": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + test "LEFT JOIN (default) - 2 value": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), (table: "invoice", tableAs: "", on: @["invoice.id = t.invoice_id", "invoice.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) LEFT JOIN invoice ON (invoice.id = t.invoice_id AND invoice.status = 1) WHERE id = ? ")) + + test "LEFT JOIN (default) - 3 value": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), (table: "invoice", tableAs: "", on: @["invoice.id = t.invoice_id", "invoice.status = 1"]), (table: "letter", tableAs: "", on: @["letter.id = t.letter_id", "letter.status = 1"])], + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) LEFT JOIN invoice ON (invoice.id = t.invoice_id AND invoice.status = 1) LEFT JOIN letter ON (letter.id = t.letter_id AND letter.status = 1) WHERE id = ? ")) + + + test "INNER JOIN": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + jointype = INNER, + hideIsDeleted = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + +suite "test sqlSelectConst - deletemarkers / softdelete": + + + + test "deletemarkers from const - 1 value": + let test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = ["tasks"], + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + test "deletemarkers from const - 2 values": + let test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = ["tasks", "history"], + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + test "deletemarkers from const - 3 values": + let test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = ["tasks", "history", "person"], + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test "deletemarkers from const": + # var test: SqlQuery + const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + let test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet, + # tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + + test "deletemarkers from inline": + var test: SqlQuery + # const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + # tablesWithDeleteMarker = [] #tableWithDeleteMarkerLet, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + + test "deletemarkers from inline (without join)": + var test: SqlQuery + # const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + # joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + # tablesWithDeleteMarker = [] #tableWithDeleteMarkerLet, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test "deletemarkers from inline with WHERE IN": + var test: SqlQuery + const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + # tablesWithDeleteMarker = [] #tableWithDeleteMarkerLet, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], + + whereInField = "tasks", + whereInValue = ["1", "2", "3"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks in (1,2,3) AND tasks.is_deleted IS NULL ")) + + + test "deletemarkers misc": + var test: SqlQuery + const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.is_deleted IS NULL AND history.is_deleted IS NULL ")) + + + + test = sqlSelectConst( + table = "tasks", + select = ["tasks.id", "tasks.name"], + where = ["tasks.id ="], + joinargs = [(table: "history", tableAs: "his", on: @["his.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, sql("SELECT tasks.id, tasks.name FROM tasks LEFT JOIN history AS his ON (his.id = tasks.hid) WHERE tasks.id = ? AND tasks.is_deleted IS NULL AND his.is_deleted IS NULL ")) + + + + test "deletemarkers on the fly": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + + test "custom deletemarker override": + var test: SqlQuery + const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], + tablesWithDeleteMarker = tableWithDeleteMarkerLet, + deleteMarker = ".deleted_at = 543234563" + ) + check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.deleted_at = 543234563 AND history.deleted_at = 543234563 ")) + + + + test "complex query": + var test: SqlQuery + + const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + + + test = sqlSelectConst( + table = "tasksitems", + tableAs = "tasks", + select = [ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + where = [ + "projects.id =", + "tasks.status >" + ], + joinargs = [ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) + ], + whereInField = "tasks.id", + whereInValue = ["1", "2", "3"], + customSQL = "ORDER BY tasks.created DESC", + tablesWithDeleteMarker = tableWithDeleteMarkerLet + ) + check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + AND tasks.is_deleted IS NULL + AND his.is_deleted IS NULL + ORDER BY + tasks.created DESC + """))) + + + + + + +suite "sqlSelectConst": + + test "with delete marker": + let a = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker + ) + + check querycompare(a, (sql(""" + SELECT + t.id, t.name, t.description, t.created, t.updated, t.completed + FROM + tasks AS t + WHERE + t.id = ? + AND t.is_deleted IS NULL + """))) + + + test "deletemarkers + joind": + let b = sqlSelectConst( + table = "tasks", + # tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id =", "status >"], + joinargs = [ + (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] #tableWithDeleteMarker + ) + + check querycompare(b, sql(""" + SELECT + id, name, description, created, updated, completed + FROM + tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + WHERE + id = ? + AND status > ? + AND tasks.is_deleted IS NULL + AND his.is_deleted IS NULL + """)) + + + let c = sqlSelectConst( + table = "tasks", + select = ["tasks.id"], + where = ["status >"], + joinargs = [ + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(c, sql(""" + SELECT + tasks.id + FROM + tasks + LEFT JOIN history ON + (his.id = tasks.hid) + WHERE + status > ? + AND tasks.is_deleted IS NULL + AND history.is_deleted IS NULL + """)) + + + test "deletemarkers + join-mess": + let d = sqlSelectConst( + table = "tasks", + select = ["tasks.id"], + where = ["status >"], + joinargs = [ + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + (table: "history", tableAs: "", on: @["his.id = tasks.hid"]), + ], + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(d, sql(""" + SELECT + tasks.id + FROM + tasks + LEFT JOIN history ON + (his.id = tasks.hid) + LEFT JOIN history ON + (his.id = tasks.hid) + LEFT JOIN history ON + (his.id = tasks.hid) + LEFT JOIN history ON + (his.id = tasks.hid) + LEFT JOIN history ON + (his.id = tasks.hid) + WHERE + status > ? + AND tasks.is_deleted IS NULL + AND history.is_deleted IS NULL + """)) + + + test "where in values (preformatted)": + let e = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name"], + where = ["t.id ="], + whereInField = "t.name", + whereInValue = ["'1aa'", "'2bb'", "'3cc'"], + tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker + ) + check querycompare(e, sql(""" + SELECT + t.id, t.name + FROM + tasks AS t + WHERE + t.id = ? + AND t.name in ('1aa','2bb','3cc')""")) + + + test "where in values (empty)": + + let f = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name"], + where = ["t.id ="], + whereInField = "t.id", + whereInValue = [""], + tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker + ) + check querycompare(f, sql("SELECT t.id, t.name FROM tasks AS t WHERE t.id = ? AND t.id in (0)")) diff --git a/tests/test_update.nim b/tests/test_update.nim new file mode 100644 index 0000000..a9f71f5 --- /dev/null +++ b/tests/test_update.nim @@ -0,0 +1,207 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder + + +proc querycompare(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 + + + +suite "update": + + test "sqlUpdate using genArgs": + let a3 = genArgs("hje", "") + discard sqlUpdate("my-table", ["name", "age"], ["id"], a3.query) + assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" + + let a4 = genArgs("hje", dbNullVal) + discard sqlUpdate("my-table", ["name", "age"], ["id"], a4.query) + assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" + + test "sqlUpdate using genArgsColumns": + let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + discard sqlUpdate("my-table", s, ["id"], a1.query) + assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" + + test "sqlUpdate using genArgsSetNull": + let a2 = genArgsSetNull("hje", "") + discard sqlUpdate("my-table", ["name", "age"], ["id"], a2.query) + assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" + + + + test "update value": + let q = sqlUpdate( + "table", + ["name", "age", "info"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ? WHERE id = ?")) + + + + test "update value with NULL": + let q = sqlUpdate( + "table", + ["name", "age", "info = NULL"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ?")) + + + + test "update value with NULL multiple": + let q = sqlUpdate( + "table", + ["name = NULL", "age", "info = NULL"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ?")) + + + + test "update value with spaces": + let q = sqlUpdate( + "table", + ["name = ", "age ", "info =", "hey = NULL "], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ?, hey = NULL WHERE id = ?")) + + + + + test "update value with WHERE params": + let q = sqlUpdate( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", "epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22", "age !="], + ) + check querycompare(q, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ?")) + + + test "update value with WHERE params with spaces": + let q2 = sqlUpdate( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", " epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22 ", " age !="], + ) + check querycompare(q2, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ?")) + + + + test "update arrays": + let q = sqlUpdate( + "table", + ["parents = ARRAY_APPEND(id, ?)", "age = ARRAY_REMOVE(id, ?)", "info = NULL"], + ["last_name NOT IN ('Anderson', 'Johnson', 'Smith')"], + ) + check querycompare(q, sql("UPDATE table SET parents = ARRAY_APPEND(id, ?), age = ARRAY_REMOVE(id, ?), info = NULL WHERE last_name NOT IN ('Anderson', 'Johnson', 'Smith')")) + + + +suite "update macro": + + test "update value": + + let q = sqlUpdateMacro( + "table", + ["name", "age", "info"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ? WHERE id = ?")) + + + test "update value": + let q = sqlUpdateMacro( + "table", + ["name", "age", "info"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ? WHERE id = ?")) + + + + test "update value with NULL": + let q = sqlUpdateMacro( + "table", + ["name", "age", "info = NULL"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ?")) + + + + test "update value with NULL multiple": + let q = sqlUpdateMacro( + "table", + ["name = NULL", "age", "info = NULL"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ?")) + + + + test "update value with spaces": + let q = sqlUpdateMacro( + "table", + ["name = ", "age ", "info =", "hey = NULL "], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ?, hey = NULL WHERE id = ?")) + + + + + test "update value with WHERE params": + let q = sqlUpdateMacro( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", "epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22", "age !="], + ) + check querycompare(q, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ?")) + + + test "update value with WHERE params with spaces": + let q2 = sqlUpdateMacro( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", " epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22 ", " age !="], + ) + check querycompare(q2, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ?")) + + + test "update arrays": + let q = sqlUpdateMacro( + "table", + ["parents = ARRAY_APPEND(id, ?)", "age = ARRAY_REMOVE(id, ?)", "info = NULL"], + ["last_name NOT IN ('Anderson', 'Johnson', 'Smith')"], + ) + check querycompare(q, sql("UPDATE table SET parents = ARRAY_APPEND(id, ?), age = ARRAY_REMOVE(id, ?), info = NULL WHERE last_name NOT IN ('Anderson', 'Johnson', 'Smith')")) \ No newline at end of file From 1187a39983a16520bef1f8235a90c1b0ef9723d3 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 20:01:19 +0200 Subject: [PATCH 05/27] Gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c970c19..0ae59ec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ tests/test +tests/* +!tests/t*.nim src/sqlbuilder .vscode \ No newline at end of file From e1af5dbeb5ad33fa308bccf58996accaa10bb866 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 20:02:01 +0200 Subject: [PATCH 06/27] Update readme --- README.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 64e1496..2050e9b 100644 --- a/README.md +++ b/README.md @@ -31,16 +31,16 @@ import sqlbuilder/select # nano sqlfile.nim # import this file instead of sqlbuilder -import src/sqlbuilderpkg/insert +import src/sqlbuilder/sqlbuilderpkg/insert export insert -import src/sqlbuilderpkg/update +import src/sqlbuilder/sqlbuilderpkg/update export update -import src/sqlbuilderpkg/delete +import src/sqlbuilder/sqlbuilderpkg/delete export delete -import src/sqlbuilderpkg/utils +import src/sqlbuilder/sqlbuilderpkg/utils export utils # This enables the softdelete columns for the legacy selector @@ -402,6 +402,90 @@ check querycompare(test, sql(""" +# Query calls for the lazy + +These are procs to catch DB errors and return a default value to move on. +This should only be used if: + - It is not critical data + - You can live with a default value in case of an error + - You have no other way to catch the error + - You are to lazy to write the try-except procs yourself + +## Import + +```nim +import sqlbuilder/query_calls +``` + +## Procs + +```nim +proc getValueTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = +``` +____ + + +```nim +proc getAllRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[Row] = +``` + +____ + + +```nim +proc getRowTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = +``` + +____ + + +```nim +proc tryExecTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): bool = +``` + +____ + + +```nim +proc execTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]) = +``` + +____ + + +```nim +proc execAffectedRowsTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = +``` + +____ + +```nim +iterator fastRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = +``` + +____ + + +# Convert result to types + +The `totypes` module contains procs to convert the result to types. + +```nim +type + Person = ref object + id: int + name: string + age: int + ident: string + is_nimmer: bool + +let + columns = @["name","id","ident"] + val = db.getRow(sql("SELECT " & columns.join(",") & " FROM my_table WHERE id = 1")) + res = sqlToType(Person, columns, val) +``` + + # Examples See the test files. From 96e49ea22f43680b04608b828a941f3b133f10b9 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Fri, 13 Oct 2023 20:20:54 +0200 Subject: [PATCH 07/27] Readme --- README.md | 151 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 2050e9b..466ecf9 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,22 @@ This library allows the user, to insert NULL values into queries and ease the creating of queries. +# TOC + +- General + - [Importing and use](#importing) + - [Macro generated queries](#macro-generated-queries) + - [NULL values](#null-values) +- Main examples + - [Examples (INSERT)](#examples-insert) + - [Examples (UPDATE)](#examples-update) + - [Examples (SELECT)](#examples-select) +- Utilities + - [Dynamic selection of columns](#dynamic-selection-of-columns) + - [Query calls for the lazy](#query-calls-for-the-lazy) + - [Convert result to types](#convert-result-to-types) +- [Examples](#examples) + # Importing @@ -65,6 +81,9 @@ macros. # NULL values +The `genArgs` and `genArgsSetNull` allows you to pass NULL values to the queries. +These are now only needed for insert-queries. + ## A NULL value @@ -187,60 +206,10 @@ is your responsibility to respect the columns. -# Dynamic selection of columns -Select which columns to include. - -Lets say, that you are importing data from a spreadsheet with static -columns, and you need to update your DB with the new values. - -If the spreadsheet contains an empty field, you can update your DB with the -NULL value. But sometimes the spreadsheet only contains 5 out of the 10 -static columns. Now you don't know the values of the last 5 columns, -which will result in updating the DB with NULL values. - -The template `genArgsColumns()` allows you to use the same query, but selecting -only the columns which shall be updated. When importing your spreadsheet, check -if the column exists (bool), and pass that as the `use: bool` param. If -the column does not exists, it will be skipped in the query. - -## Insert & Delete -```nim - let (s, a) = genArgsColumns((true, "name", "Thomas"), (true, "age", 30), (false, "nim", "never")) - # We are using the column `name` and `age` and ignoring the column `nim`. - - echo $a.args - # ==> Args: @["Thomas", "30"] - - let a1 = sqlInsert("my-table", s, a.query) - # ==> INSERT INTO my-table (name, age) VALUES (?, ?) - - let a2 = sqlDelete("my-table", s, a.query) - # ==> DELETE FROM my-table WHERE name = ? AND age = ? -``` - -## Update & Select -```nim - let (s, a) = genArgsColumns((true, "name", "Thomas"), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - # We are using the column `name` and `age` and ignoring the column `nim`. We - # are using the value `154` as our identifier, therefor the column is not - # specified - - echo $a.args - # ==> Args: @["Thomas", "30", "154"] - - let a3 = sqlUpdate("my-table", s, ["id"], a.query) - # ==> UPDATE my-table SET name = ?, age = ? WHERE id = ? - - let a4 = sqlSelect("my-table", s, [""], ["id ="], "", "", "", a.query) - # ==> SELECT name, age FROM my-table WHERE id = ? -``` - - - # Examples (INSERT) -## Insert without NULL +## Insert default ```nim exec(db, sqlInsert("myTable", ["email", "age"]), "em@em.com" , 20) @@ -248,7 +217,12 @@ the column does not exists, it will be skipped in the query. insertID(db, sqlInsert("myTable", ["email", "age"]), "em@em.com", 20) # ==> INSERT INTO myTable (email, age) VALUES (?, ?) ``` - +```nim + exec(db, sqlInsertMacro("myTable", ["email", "age"]), "em@em.com" , 20) + # OR + insertID(db, sqlInsertMacro("myTable", ["email", "age"]), "em@em.com", 20) + # ==> INSERT INTO myTable (email, age) VALUES (?, ?) +``` ## Insert with NULL @@ -261,6 +235,27 @@ the column does not exists, it will be skipped in the query. ``` +# Examples (UPDATE) + +```nim + # sqlUpdate or sqlUpdateMacro + let q = sqlUpdate("table", ["name", "age", "info = NULL"], ["id ="]) + # ==> UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ? +``` + +```nim + # sqlUpdate or sqlUpdateMacro + let q = sqlUpdate( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", "epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22", "age !="], + ) + # ==> UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ? +``` + + + + # Examples (SELECT) Please note that you have to define the equal symbol for the where clause. This @@ -402,6 +397,56 @@ check querycompare(test, sql(""" +# Dynamic selection of columns +Select which columns to include. + +Lets say, that you are importing data from a spreadsheet with static +columns, and you need to update your DB with the new values. + +If the spreadsheet contains an empty field, you can update your DB with the +NULL value. But sometimes the spreadsheet only contains 5 out of the 10 +static columns. Now you don't know the values of the last 5 columns, +which will result in updating the DB with NULL values. + +The template `genArgsColumns()` allows you to use the same query, but selecting +only the columns which shall be updated. When importing your spreadsheet, check +if the column exists (bool), and pass that as the `use: bool` param. If +the column does not exists, it will be skipped in the query. + +## Insert & Delete +```nim + let (s, a) = genArgsColumns((true, "name", "Thomas"), (true, "age", 30), (false, "nim", "never")) + # We are using the column `name` and `age` and ignoring the column `nim`. + + echo $a.args + # ==> Args: @["Thomas", "30"] + + let a1 = sqlInsert("my-table", s, a.query) + # ==> INSERT INTO my-table (name, age) VALUES (?, ?) + + let a2 = sqlDelete("my-table", s, a.query) + # ==> DELETE FROM my-table WHERE name = ? AND age = ? +``` + +## Update & Select +```nim + let (s, a) = genArgsColumns((true, "name", "Thomas"), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + # We are using the column `name` and `age` and ignoring the column `nim`. We + # are using the value `154` as our identifier, therefor the column is not + # specified + + echo $a.args + # ==> Args: @["Thomas", "30", "154"] + + let a3 = sqlUpdate("my-table", s, ["id"], a.query) + # ==> UPDATE my-table SET name = ?, age = ? WHERE id = ? + + let a4 = sqlSelect("my-table", s, [""], ["id ="], "", "", "", a.query) + # ==> SELECT name, age FROM my-table WHERE id = ? +``` + + + # Query calls for the lazy These are procs to catch DB errors and return a default value to move on. @@ -488,7 +533,7 @@ let # Examples -See the test files. +See the test files in `tests/` for more examples. From 59aaaf6a294c46a064516838e6d577cf9e81154c Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:47:09 +0100 Subject: [PATCH 08/27] query: delete --- src/sqlbuilderpkg/delete.nim | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/src/sqlbuilderpkg/delete.nim b/src/sqlbuilderpkg/delete.nim index ee282b6..4ca7fb8 100644 --- a/src/sqlbuilderpkg/delete.nim +++ b/src/sqlbuilderpkg/delete.nim @@ -1,18 +1,17 @@ # Copyright 2020 - Thomas T. Jarløv when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import - std/macros, - std/strutils + std/macros import - ./utils + ./utils_private + +from ./utils import ArgsContainer proc sqlDelete*(table: string, where: varargs[string]): SqlQuery = @@ -24,11 +23,7 @@ proc sqlDelete*(table: string, where: varargs[string]): SqlQuery = for i, d in where: if i > 0: wes.add(" AND ") - wes.add(d & " = ?") - - when defined(testSqlquery): - echo res & wes - + wes.add(formatWhereParams(d)) result = sql(res & wes) @@ -45,10 +40,6 @@ proc sqlDelete*(table: string, where: varargs[string], args: ArgsContainer.query wes.add(d & " = NULL") else: wes.add(d & " = ?") - - when defined(testSqlquery): - echo res & wes - result = sql(res & wes) @@ -61,9 +52,5 @@ macro sqlDeleteMacro*(table: string, where: varargs[string]): SqlQuery = for i, d in where: if i > 0: wes.add(" AND ") - wes.add($d & " = ?") - - when defined(testSqlquery): - echo res & wes - + wes.add(formatWhereParams($d)) result = parseStmt("sql(\"" & res & wes & "\")") From 382af4fc4a4b5f767f4a4e55938d16bdceac1939 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:47:31 +0100 Subject: [PATCH 09/27] query: insert --- src/sqlbuilderpkg/insert.nim | 79 +++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/src/sqlbuilderpkg/insert.nim b/src/sqlbuilderpkg/insert.nim index cb03398..3e9b807 100644 --- a/src/sqlbuilderpkg/insert.nim +++ b/src/sqlbuilderpkg/insert.nim @@ -1,19 +1,15 @@ # Copyright 2020 - Thomas T. Jarløv when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/macros, std/strutils -import - ./utils - +from ./utils import ArgsContainer proc sqlInsert*(table: string, data: varargs[string], args: ArgsContainer.query): SqlQuery = ## SQL builder for INSERT queries @@ -30,38 +26,63 @@ proc sqlInsert*(table: string, data: varargs[string], args: ArgsContainer.query) fields.add(d) vals.add('?') - when defined(testSqlquery): - echo fields & ") VALUES (" & vals & ")" - - when defined(test): - testout = fields & ") VALUES (" & vals & ")" - result = sql(fields & ") VALUES (" & vals & ")") -proc sqlInsert*(table: string, data: varargs[string]): SqlQuery = +proc sqlInsert*(table: string, data: varargs[string], args: seq[string] = @[]): SqlQuery = ## SQL builder for INSERT queries - ## Does NOT check for NULL values + ## + ## Can check for NULL values manually typed or by comparing + ## the length of the data and args sequences. + ## + ## data = @["id", "name", "age"] + ## args = @["1", "Thomas", "NULL"] + ## => INSERT INTO table (id, name, age) VALUES (?, ?, NULL) + ## + ## data = @["id", "name", "age"] + ## args = @["1", "Thomas"] or @[] + ## => INSERT INTO table (id, name, age) VALUES (?, ?, ?) + + var + fields = "INSERT INTO " & table & " (" + vals = "" + + let + checkArgs = data.len() == args.len() + - var fields = "INSERT INTO " & table & " (" - var vals = "" for i, d in data: - # New value if i > 0: fields.add(", ") vals.add(", ") + # + # Check for manual null and then short circuit + # + if d.endsWith(" = NULL"): + fields.add(d.split(" = NULL")[0]) + vals.add("NULL") + continue + + # # Insert field name + # fields.add(d) + # # Insert value parameter - vals.add('?') - - when defined(testSqlquery): - echo fields & ") VALUES (" & vals & ")" - - when defined(test): - testout = fields & ") VALUES (" & vals & ")" + # + # Check corresponding args + if checkArgs: + if args[i].len() == 0: + vals.add("NULL") + else: + vals.add('?') + # + # No args, just add parameter + # + else: + vals.add('?') result = sql(fields & ") VALUES (" & vals & ")") @@ -76,10 +97,10 @@ macro sqlInsertMacro*(table: string, data: varargs[string]): SqlQuery = if i > 0: fields.add(", ") vals.add(", ") + if ($d).endsWith(" = NULL"): + fields.add(($d).split(" = NULL")[0]) + vals.add("NULL") + continue fields.add($d) vals.add('?') - - when defined(testSqlquery): - echo fields & ") VALUES (" & vals & ")" - result = parseStmt("sql(\"" & $fields & ") VALUES (" & $vals & ")\")") From 69b7813800f749db6bf7172673ec48550102cf90 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:49:52 +0100 Subject: [PATCH 10/27] query: select --- src/sqlbuilderpkg/select.nim | 474 +++++++++++------------------------ 1 file changed, 151 insertions(+), 323 deletions(-) diff --git a/src/sqlbuilderpkg/select.nim b/src/sqlbuilderpkg/select.nim index 4a5d4df..9cfe5f4 100644 --- a/src/sqlbuilderpkg/select.nim +++ b/src/sqlbuilderpkg/select.nim @@ -1,22 +1,20 @@ # Copyright 2020 - Thomas T. Jarløv when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/macros, std/strutils import - ./utils, ./utils_private +from ./utils import SQLJoinType, ArgsContainer - +const noJoin*: tuple[table: string, tableAs: string, on: seq[string]] = ("", "", @[]) ## @@ -31,10 +29,7 @@ proc sqlSelectConstSelect(select: varargs[string]): string = proc sqlSelectConstJoin( joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]], - jointype: NimNode, - # tablesWithDeleteMarker: varargs[string], - # hideIsDeleted: NimNode, - # deleteMarker: NimNode, + jointype: NimNode ): string = var lef = "" @@ -58,36 +53,16 @@ proc sqlSelectConstJoin( lef.add(" AND ") lef.add($join) - # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: - # var hit = false - # for t in tablesWithDeleteMarker: - # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: - # continue - - # if $t == $d.table: - # hit = true - # break - - # if hit: - # lef.add( - # " AND " & - # ( - # if d.tableAs.len() > 0 and $d.tableAs != $d.table: - # $d.tableAs - # else: - # $d.table - # ) & - # $deleteMarker - # ) - lef.add(")") return lef -proc sqlSelectConstWhere(where: varargs[string]): string = +proc sqlSelectConstWhere(where: varargs[string], usePrepared: NimNode): string = - var wes = "" + var + wes = "" + prepareCount = 0 for i, d in where: let v = $d @@ -106,19 +81,35 @@ proc sqlSelectConstWhere(where: varargs[string]): string = # => ? = ANY(...) elif v.len() > 5 and v[0..4] == "= ANY": - wes.add("? " & v) + if boolVal(usePrepared): + prepareCount += 1 + wes.add("$" & $prepareCount & " " & v) + else: + wes.add("? " & v) # => ... IN (?) elif v[(v.high - 2)..v.high] == " IN": - wes.add(v & " (?)") + if boolVal(usePrepared): + prepareCount += 1 + wes.add(v & " ($" & $prepareCount & ")") + else: + wes.add(v & " (?)") # => ? IN (...) elif v.len() > 2 and v[0..1] == "IN": - wes.add("? " & v) + if boolVal(usePrepared): + prepareCount += 1 + wes.add("$" & $prepareCount & " " & v) + else: + wes.add("? " & v) # => ... = ? else: - wes.add(v & " ?") + if boolVal(usePrepared): + prepareCount += 1 + wes.add(v & " $" & $prepareCount) + else: + wes.add(v & " ?") return wes @@ -151,18 +142,16 @@ proc sqlSelectConstWhereIn( proc sqlSelectConstSoft( wes, acc: string, - # table, tableAs: NimNode, tablesInQuery: seq[tuple[table: string, tableAs: string]], tablesWithDeleteMarker: varargs[string], - hideIsDeleted: NimNode, + useDeleteMarker: NimNode, deleteMarker: NimNode ): (string, string) = - - if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: + if $useDeleteMarker == "true" and tablesWithDeleteMarker.len() > 0: var wesTo, accTo: string for t in tablesInQuery: - if t.table notin tablesWithDeleteMarker: # and t.tableAs notin tablesWithDeleteMarker: + if t.table notin tablesWithDeleteMarker: continue let toUse = if t.tableAs != "": t.tableAs else: t.table @@ -178,41 +167,11 @@ proc sqlSelectConstSoft( return (wesTo, accTo) - # var hit = false - # for t in tablesWithDeleteMarker: - # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: - # continue - - # if $t == $table: - # hit = true - # break - - - # if hit: - # let tableNameToUse = - # if ($tableAs).len() > 0 and $tableAs != $table: - # $tableAs - # else: - # $table - - # var wesTo, accTo: string - - # if wes == "" and acc == "": - # wesTo.add(" WHERE " & $tableNameToUse & $deleteMarker) - - # elif acc != "": - # accTo.add(" AND " & $tableNameToUse & $deleteMarker) - - # else: - # wesTo.add(" AND " & $tableNameToUse & $deleteMarker) - - # return (wesTo, accTo) ## ## Constant generator ## -const deadTuple: tuple[table: string, tableAs: string, on: seq[string]] = ("", "", @[]) macro sqlSelectConst*( # BASE table: string, @@ -231,39 +190,37 @@ macro sqlSelectConst*( customSQL: string = "", # Table alias - tableAs: string = "",# = $table, + tableAs: string = "", + + # Prepare statement + usePrepared: bool = false, # Soft delete - hideIsDeleted: bool = true, - tablesWithDeleteMarker: static varargs[string] = [], + useDeleteMarker: bool = true, + # If we are using `static` then we can assign const-variables to it. It not, + # we must use direct varargs. + tablesWithDeleteMarker: varargs[string] = [], deleteMarker = ".is_deleted IS NULL", - testValue: string = "", ): SqlQuery = ## SQL builder for SELECT queries - # - # Set delete fields - # - # 1) macro options - # 2) default options set outside macro - # var deleteMarkersFields: seq[string] - if $(tablesWithDeleteMarker.repr) != "[]": + if tablesWithDeleteMarker.len() > 0: for t in tablesWithDeleteMarker: - if t.repr.len == 0:# or tablesWithDeleteMarker.len() == 2: + if t.repr.len == 0: + continue + if $t notin deleteMarkersFields: + deleteMarkersFields.add($t) + + when declared(tablesWithDeleteMarkerInit): + for t in tablesWithDeleteMarkerInit: + if t.repr.len == 0: continue if t notin deleteMarkersFields: deleteMarkersFields.add(t) - when declared(tablesWithDeleteMarkerInit): - if $(tablesWithDeleteMarker.repr) != "[]": - for t in tablesWithDeleteMarkerInit: - if t.repr.len == 0:# or tablesWithDeleteMarkerInit.len() == 2: - continue - if t notin deleteMarkersFields: - deleteMarkersFields.add(t) # # Create seq of tables @@ -278,9 +235,9 @@ macro sqlSelectConst*( # Join table var joinTablesUsed: seq[string] - if joinargs != [] and $(joinargs.repr) != "[]": + if joinargs[0] != noJoin and joinargs != [] and $(joinargs.repr) != "[]": for i, d in joinargs: - if d.repr.len == 0:# or joinargs.len() == 2: + if d.repr.len == 0: continue if $d.table in joinTablesUsed: @@ -313,96 +270,20 @@ macro sqlSelectConst*( "Bug: `select.len() == 0` in \n" & $select ) var res = sqlSelectConstSelect(select) - # var res = "SELECT " - - # for i, d in select: - # if i > 0: res.add(", ") - # res.add($d) # # Joins # - var lef = sqlSelectConstJoin( - joinargs, jointype) - # deleteMarkersFields, hideIsDeleted, deleteMarker) - # var lef = "" - - # for d in joinargs: - # if d.repr.len == 0 and joinargs.len() == 2: - # break - - # lef.add(" " & $jointype & " JOIN ") - # lef.add($d.table & " ") - - # if d.tableAs != "" and d.tableAs != d.table: - # lef.add("AS " & $d.tableAs & " ") - - # lef.add("ON (") - - # for i, join in d.on: - # if i > 0: - # lef.add(" AND ") - # lef.add($join) - - # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: - # var hit = false - # for t in tablesWithDeleteMarker: - # if $t == $d.table: - # hit = true - # break - - # if hit: - # lef.add( - # " AND " & - # ( - # if d.tableAs.len() > 0 and $d.tableAs != $d.table: - # $d.tableAs - # else: - # $d.table - # ) & - # $deleteMarker - # ) - - # lef.add(")") + var lef = "" + if joinargs[0] != noJoin: + lef = sqlSelectConstJoin(joinargs, jointype) # # Where - normal # - var wes = sqlSelectConstWhere(where) - # var wes = "" - - # for i, d in where: - # let v = $d - - # if v.len() > 0 and i == 0: - # wes.add(" WHERE ") - - # if i > 0: - # wes.add(" AND ") - - # if v.len() > 0: - - # # => ... = NULL - # if v[(v.high - 3)..v.high] == "NULL": - # wes.add(v) - - # # => ? = ANY(...) - # elif v.len() > 5 and v[0..4] == "= ANY": - # wes.add("? " & v) - - # # => ... IN (?) - # elif v[(v.high - 2)..v.high] == " IN": - # wes.add(v & " (?)") - - # # => ? IN (...) - # elif v.len() > 2 and v[0..1] == "IN": - # wes.add("? " & v) - - # # => ... = ? - # else: - # wes.add(v & " ?") + var wes = sqlSelectConstWhere(where, usePrepared) @@ -410,61 +291,20 @@ macro sqlSelectConst*( # Where - n IN (x,c,v) # var acc = "" - acc.add sqlSelectConstWhereIn(wes, acc, whereInField, whereInValue) - # if ($whereInField).len() > 0 and (whereInValue).len() > 0: - # if wes.len == 0: - # acc.add(" WHERE " & $whereInField & " in (") - # else: - # acc.add(" AND " & $whereInField & " in (") - - - # var inVal: string - - # for a in whereInValue: - # if inVal != "": - # inVal.add(",") - # inVal.add($a) - - # acc.add(if inVal == "": "0" else: inVal) - # acc.add(")") - - # # Soft delete # var (toWes, toAcc) = sqlSelectConstSoft( wes, acc, - # table, tableAs, tablesInQuery, deleteMarkersFields, - hideIsDeleted, deleteMarker + useDeleteMarker, deleteMarker ) wes.add(toWes) acc.add(toAcc) - # if $hideIsDeleted == "true" and tablesWithDeleteMarker.len() > 0: - # var hit = false - # for t in tablesWithDeleteMarker: - # if $t == $table: - # hit = true - # break - - # if hit: - # let tableNameToUse = - # if ($tableAs).len() > 0 and $tableAs != $table: - # $tableAs - # else: - # $table - - # if wes == "" and acc == "": - # wes.add(" WHERE " & $tableNameToUse & $deleteMarker) - # elif acc != "": - # acc.add(" AND " & $tableNameToUse & $deleteMarker) - # else: - # wes.add(" AND " & $tableNameToUse & $deleteMarker) - # # Combine the pretty SQL @@ -475,47 +315,25 @@ macro sqlSelectConst*( # # Error checking # - # => Bad format - const illegalFormats = [ - "WHERE AND", - "WHERE OR", - "AND AND", - "OR OR", - "AND OR", - "OR AND", - "WHERE IN", - "WHERE =", - "WHERE >", - "WHERE <", - "WHERE !", - "WHERE LIKE", - "WHERE NOT", - "WHERE IS", - "WHERE NULL", - "WHERE ANY" - ] - - for f in illegalFormats: - if f in finalSQL: - raise newException( - Exception, - "Bad SQL format. Please check your SQL statement. " & - "This is most likely caused by a missing WHERE clause. " & - "Bug: `" & f & "` in \n" & finalSQL - ) + + var illegal = hasIllegalFormats($finalSQL) + if illegal.len() > 0: + raise newException( + Exception, + "Bad SQL format. Please check your SQL statement. " & + "This is most likely caused by a missing WHERE clause. " & + "Bug: `" & illegal & "` in \n" & finalSQL + ) if $table != $tableAs and lef.len() > 0: var hit: bool for s in select: if "." notin s: - echo "WARNING: Missing table alias in select statement: " & $s + echo "WARNING (SQL MACRO): Missing table alias in select statement: " & $s hit = true if hit: - echo "WARNING: " & finalSQL + echo "WARNING (SQL MACRO): " & finalSQL - when defined(verboseSqlquery): - echo "SQL Macro:" - echo finalSQL result = parseStmt("sql(\"" & finalSQL & "\")") @@ -529,38 +347,40 @@ proc sqlSelect*( table: string, select: varargs[string], where: varargs[string], + # Join joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], jointype: SQLJoinType = LEFT, - joinoverride: string = "", + joinoverride: string = "", # Override the join statement by inserting without check + # WHERE-IN whereInField: string = "", - whereInValue: seq[string] = @[], # Could be unsafe. Is not checked. + whereInValue: varargs[string] = [], # Could be unsafe. Is not checked. whereInValueString: seq[string] = @[], whereInValueInt: seq[int] = @[], + # Custom SQL, e.g. ORDER BY customSQL: string = "", + # Null checks checkedArgs: ArgsContainer.query = @[], + # Table alias tableAs: string = table, + + # Prepare statement + usePrepared: bool = false, + # Soft delete - hideIsDeleted: bool = true, + useDeleteMarker: bool = true, tablesWithDeleteMarker: varargs[string] = [], #(when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], deleteMarker = ".is_deleted IS NULL", ): SqlQuery = ## SQL builder for SELECT queries - # - # Set delete fields - # - # 1) macro options - # 2) default options set outside macro - # + var deleteMarkersFields: seq[string] for t in tablesWithDeleteMarker: - # if t.repr.len == 0 or tablesWithDeleteMarker.len() == 2: - # continue if t == "": continue if t notin deleteMarkersFields: @@ -570,8 +390,6 @@ proc sqlSelect*( for t in tablesWithDeleteMarkerInit: if t == "": continue - # if t.repr.len == 0 or tablesWithDeleteMarkerInit.len() == 2: - # continue if t notin deleteMarkersFields: deleteMarkersFields.add(t) @@ -580,23 +398,23 @@ proc sqlSelect*( # var tablesInQuery: seq[tuple[table: string, tableAs: string]] + # Base table if $tableAs != "" and $table != $tableAs: tablesInQuery.add(($table, $tableAs)) else: tablesInQuery.add(($table, "")) - # Join table - for d in joinargs: - # if d.repr.len == 0 and joinargs.len() == 2: - # continue - if d.table == "": - continue - if d.tableAs != "" and d.tableAs != d.table: - tablesInQuery.add((d.table, d.tableAs)) - else: - tablesInQuery.add((d.table, "")) + # Join table + if joinargs.len() > 0 and joinargs[0] != noJoin: + for d in joinargs: + if d.table == "": + continue + if d.tableAs != "" and d.tableAs != d.table: + tablesInQuery.add((d.table, d.tableAs)) + else: + tablesInQuery.add((d.table, "")) # @@ -613,7 +431,6 @@ proc sqlSelect*( # Select # var res = "SELECT " - for i, d in select: if i > 0: res.add(", ") res.add(d) @@ -623,33 +440,22 @@ proc sqlSelect*( # Joins # var lef = "" + if joinargs.len() > 0 and joinargs[0] != noJoin: + for i, d in joinargs: + lef.add(" " & $jointype & " JOIN ") + lef.add(d.table & " ") - for i, d in joinargs: - lef.add(" " & $jointype & " JOIN ") - lef.add(d.table & " ") + if d.tableAs != "" and d.tableAs != d.table: + lef.add("AS " & d.tableAs & " ") - if d.tableAs != "" and d.tableAs != d.table: - lef.add("AS " & d.tableAs & " ") + lef.add("ON (") - lef.add("ON (") + for i, join in d.on: + if i > 0: + lef.add(" AND ") + lef.add(join) - for i, join in d.on: - if i > 0: - lef.add(" AND ") - lef.add(join) - - # if hideIsDeleted and tablesWithDeleteMarker.len() > 0 and d.table in tablesWithDeleteMarker: - # lef.add( - # " AND " & - # ( - # if d.tableAs != "" and d.tableAs != d.table: - # d.tableAs - # else: - # d.table - # ) & - # deleteMarker - # ) - lef.add(")") + lef.add(")") if joinoverride.len() > 0: lef.add(" " & joinoverride) @@ -658,7 +464,9 @@ proc sqlSelect*( # # Where - normal # - var wes = "" + var + wes = "" + prepareCount = 0 for i, d in where: if d != "" and i == 0: wes.add(" WHERE ") @@ -677,26 +485,41 @@ proc sqlSelect*( # => ? = ANY(...) elif d.len() > 5 and d[0..4] == "= ANY": - wes.add("? " & d) + if usePrepared: + prepareCount += 1 + wes.add("$" & $prepareCount & " " & d) + else: + wes.add("? " & d) # => ... IN (?) elif d[(d.high - 2)..d.high] == " IN": - wes.add(d & " (?)") + if usePrepared: + prepareCount += 1 + wes.add(d & " ($" & $prepareCount & ")") + else: + wes.add(d & " (?)") # => ? IN (...) elif d.len() > 2 and d[0..1] == "IN": - wes.add("? " & d) + if usePrepared: + prepareCount += 1 + wes.add("$" & $prepareCount & " " & d) + else: + wes.add("? " & d) # => ... = ? else: - wes.add(d & " ?") + if usePrepared: + prepareCount += 1 + wes.add(d & " $" & $prepareCount) + else: + wes.add(d & " ?") # # Where IN # var acc = "" - if whereInField != "" and (whereInValue.len() > 0 or whereInValueString.len() > 0 or whereInValueInt.len() > 0): if wes.len == 0: acc.add(" WHERE " & whereInField & " in (") @@ -738,11 +561,10 @@ proc sqlSelect*( - # # Soft delete # - if hideIsDeleted and deleteMarkersFields.len() > 0: + if useDeleteMarker and deleteMarkersFields.len() > 0: for t in tablesInQuery: if t.table notin deleteMarkersFields: continue @@ -759,28 +581,34 @@ proc sqlSelect*( wes.add(" AND " & toUse & $deleteMarker) - # let tableNameToUse = - # if tableAs.len() > 0 and tableAs != table: - # tableAs - # else: - # table - - # if wes == "" and acc == "": - # wes.add(" WHERE " & tableNameToUse & deleteMarker) - # elif acc != "": - # acc.add(" AND " & tableNameToUse & deleteMarker) - # else: - # wes.add(" AND " & tableNameToUse & deleteMarker) + when defined(dev): + let sqlString = res & " FROM " & tableName & lef & wes & acc & " " & customSQL + let illegal = hasIllegalFormats(sqlString) + if illegal.len() > 0: + raise newException( + Exception, + "Bad SQL format. Please check your SQL statement. " & + "This is most likely caused by a missing WHERE clause. " & + "Bug: `" & illegal & "` in \n" & sqlString + ) - # - # Finalize - when defined(verboseSqlquery): - echo res & " FROM " & tableName & lef & wes & acc & " " & customSQL + # Check for missing table alias + if ( + (tableAs != "" and table != tableAs) or + (joinargs.len() > 0 and joinargs[0] != noJoin) + ): + var hit: bool + for s in select: + if "." notin s: + echo "WARNING (SQL SELECT): Missing table alias in select statement: " & $s + hit = true + if hit: + echo "WARNING (SQL SELECT): " & sqlString result = sql(res & " FROM " & tableName & lef & wes & acc & " " & customSQL) @@ -799,8 +627,8 @@ proc sqlSelect*( access: string, accessC: string, user: string, args: ArgsContainer.query = @[], - hideIsDeleted: bool = true, - tablesWithDeleteMarker: varargs[string] = [], #(when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], + useDeleteMarker: bool = true, + tablesWithDeleteMarker: varargs[string] = [], deleteMarker = ".is_deleted IS NULL", ): SqlQuery {.deprecated.} = ## @@ -858,7 +686,7 @@ proc sqlSelect*( whereInValue = @[access], customSQL = user, checkedArgs = args, - hideIsDeleted = hideIsDeleted, + useDeleteMarker = useDeleteMarker, tablesWithDeleteMarker = tablesWithDeleteMarker, deleteMarker = deleteMarker ) From 634c5198ca8c15844570270890dc2cb25f1a9ecd Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:50:11 +0100 Subject: [PATCH 11/27] query: update --- src/sqlbuilderpkg/update.nim | 224 +++++++++++++++++++++-------------- 1 file changed, 132 insertions(+), 92 deletions(-) diff --git a/src/sqlbuilderpkg/update.nim b/src/sqlbuilderpkg/update.nim index f461282..82bcaa9 100644 --- a/src/sqlbuilderpkg/update.nim +++ b/src/sqlbuilderpkg/update.nim @@ -1,21 +1,24 @@ # Copyright 2020 - Thomas T. Jarløv when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/macros, std/strutils import - ./utils + ./utils_private + +from ./utils import ArgsContainer proc updateSetFormat(v: string): string = + ## The table columns that we want to update. Validate whether + ## the user has provided equal sign or not. + let field = v.strip() fieldSplit = field.split("=") @@ -25,18 +28,22 @@ proc updateSetFormat(v: string): string = # if fieldSplit.len() == 2: # - # If the data is only having equal but no value, insert a `?` sign + # If the data is only having equal but no value, insert a `?` sign. + # Eg. `field =` # if fieldSplit[1] == "": return (field & " ?") + # # Otherwise just add the data as is, eg. `field = value` + # Eg. `field = value` # else: return (field) # # Otherwise revert to default + # Eg. `field = ?` # else: return (field & " = ?") @@ -44,74 +51,40 @@ proc updateSetFormat(v: string): string = proc updateSet(data: varargs[string]): string = + ## Update the SET part of the query. + ## + ## => ["name", "age = "] + ## => `SET name = ?, age = ?` + ## for i, d in data: if i > 0: result.add(", ") - result.add(updateSetFormat(d)) - return result -proc updateWhereFormat(v: string): string = - let - field = v.strip() - - var fieldSplit: seq[string] - if field.contains(" "): - if field.contains("="): - fieldSplit = field.split("=") - elif field.contains("IS NOT"): - fieldSplit = field.split("IS NOT") - elif field.contains("IS"): - fieldSplit = field.split("IS") - elif field.contains("NOT IN"): - fieldSplit = field.split("NOT IN") - elif field.contains("IN"): - fieldSplit = field.split("IN") - elif field.contains("!="): - fieldSplit = field.split("!=") - elif field.contains("<="): - fieldSplit = field.split("<=") - elif field.contains(">="): - fieldSplit = field.split(">=") - elif field.contains("<"): - fieldSplit = field.split("<") - elif field.contains(">"): - fieldSplit = field.split(">") - else: - fieldSplit = field.split("=") - - # - # Does the data have a `=` sign? - # - if fieldSplit.len() == 2: - # - # If the data is only having equal but no value, insert a `?` sign - # - if fieldSplit[1] == "": - return (field & " ?") - # - # Otherwise just add the data as is, eg. `field = value` - # - else: - return (field) - - # - # Otherwise revert to default - # - else: - return (field & " = ?") +proc updateArray(arrayType: string, arrayAppend: varargs[string]): string = + ## Format the arrayAppend part of the query. + for i, d in arrayAppend: + if i > 0: + result.add(", ") + result.add(d & " = " & arrayType & "(" & d & ", ?)") + return result proc updateWhere(where: varargs[string]): string = + ## Update the WHERE part of the query. + ## + ## => ["name", "age = "] + ## => `WHERE name = ?, age = ?` + ## + ## => ["name = ", "age >"] + ## => `WHERE name = ?, age > ?` var wes = " WHERE " for i, v in where: if i > 0: wes.add(" AND ") - - wes.add(updateWhereFormat(v)) - + wes.add(formatWhereParams(v)) return wes @@ -141,79 +114,146 @@ proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], ar else: wes.add(d & " = ?") - when defined(testSqlquery): - echo fields & wes - - when defined(test): - testout = fields & wes - result = sql(fields & wes) proc sqlUpdate*( table: string, data: varargs[string], - where: varargs[string] + where: varargs[string], ): SqlQuery = ## SQL builder for UPDATE queries - ## Does NOT check for NULL values - - var fields = "UPDATE " & table & " SET " - + ## + ## Can utilize custom equal signs and can also check for NULL values + ## + ## data => ["name", "age = ", "email = NULL"] + ## where => ["id = ", "name IS NULL"] + var fields: string fields.add(updateSet(data)) - fields.add(updateWhere(where)) + result = sql("UPDATE " & table & " SET " & fields) - when defined(testSqlquery): - echo fields - - when defined(test): - testout = fields +proc sqlUpdateArrayRemove*( + table: string, + arrayRemove: varargs[string], + where: varargs[string], + ): SqlQuery = + ## ARRAY_REMOVE + var fields: string + fields.add(updateArray("ARRAY_REMOVE", arrayRemove)) + fields.add(updateWhere(where)) + result = sql("UPDATE " & table & " SET " & fields) - result = sql(fields) +proc sqlUpdateArrayAppend*( + table: string, + arrayAppend: varargs[string], + where: varargs[string], + ): SqlQuery = + ## ARRAY_APPEND + var fields: string + fields.add(updateArray("ARRAY_APPEND", arrayAppend)) + fields.add(updateWhere(where)) + result = sql("UPDATE " & table & " SET " & fields) +# +# +# Macro based +# +# proc updateSet(data: NimNode): string = + ## Update the SET part of the query. + ## + ## => ["name", "age = "] + ## => `SET name = ?, age = ?` + ## for i, v in data: # Convert NimNode to string let d = $v - if i > 0: result.add(", ") - result.add(updateSetFormat(d)) - return result + proc updateWhere(where: NimNode): string = + ## Update the WHERE part of the query. + ## + ## => ["name", "age = "] + ## => `WHERE name = ?, age = ?` + ## + ## => ["name = ", "age >"] + ## => `WHERE name = ?, age > ?` var wes = " WHERE " for i, v in where: # Convert NimNode to string let d = $v - if i > 0: wes.add(" AND ") - - wes.add(updateWhereFormat(d)) - + wes.add(formatWhereParams(d)) return wes -macro sqlUpdateMacro*(table: string, data: varargs[string], where: varargs[string]): SqlQuery = + +macro sqlUpdateMacro*( + table: string, + data: varargs[string], + where: varargs[string] + ): SqlQuery = ## SQL builder for UPDATE queries - ## Does NOT check for NULL values + ## + ## Can utilize custom equal signs and can also check for NULL values + ## + ## data => ["name", "age = ", "email = NULL"] + ## where => ["id = ", "name IS NULL"] + var fields: string + fields.add(updateSet(data)) + fields.add(updateWhere(where)) + result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") - var fields = "UPDATE " & $table & " SET " - fields.add(updateSet(data)) +# +# Macro arrays +# +proc updateArray(arrayType: string, arrayAppend: NimNode): string = + # Format the arrayAppend part of the query. + + for i, v in arrayAppend: + let d = $v + if d == "": + continue + + if i > 0: + result.add(", ") + result.add(d & " = " & $arrayType & "(" & d & ", ?)") + + return result + + +macro sqlUpdateMacroArrayRemove*( + table: string, + arrayRemove: varargs[string], + where: varargs[string], + ): SqlQuery = + ## ARRAY_REMOVE macro + var fields: string + fields.add(updateArray("ARRAY_REMOVE", arrayRemove)) fields.add(updateWhere(where)) + result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") - when defined(testSqlquery): - echo fields - result = parseStmt("sql(\"" & fields & "\")") \ No newline at end of file +macro sqlUpdateMacroArrayAppend*( + table: string, + arrayAppend: varargs[string], + where: varargs[string], + ): SqlQuery = + ## ARRAY_APPEND macro + var fields: string + fields.add(updateArray("ARRAY_APPEND", arrayAppend)) + fields.add(updateWhere(where)) + result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") From c454f2dcdf70800f28eb02cf3377296678ea4f4e Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:50:26 +0100 Subject: [PATCH 12/27] query: utils_private --- src/sqlbuilderpkg/utils_private.nim | 130 +++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/src/sqlbuilderpkg/utils_private.nim b/src/sqlbuilderpkg/utils_private.nim index ccf0505..a6d610e 100644 --- a/src/sqlbuilderpkg/utils_private.nim +++ b/src/sqlbuilderpkg/utils_private.nim @@ -1,5 +1,31 @@ # Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk +when NimMajor >= 2: + import db_connector/db_common +else: + import std/db_common + + +import + std/strutils + + +proc querycompare*(a, b: SqlQuery): bool = + var + a1: seq[string] + b1: seq[string] + for c in splitWhitespace(string(a)): + a1.add($c) + for c in splitWhitespace(string(b)): + b1.add($c) + + if a1 != b1: + echo "" + echo "a1: ", string(a) + echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") + echo "" + + return a1 == b1 proc dbQuotePrivate*(s: string): string = @@ -10,4 +36,106 @@ proc dbQuotePrivate*(s: string): string = of '\'': add(result, "''") of '\0': add(result, "\\0") else: add(result, c) - add(result, '\'') \ No newline at end of file + add(result, '\'') + + + +proc formatWhereParams*(v: string): string = + ## Format the WHERE part of the query. + let + field = v.strip() + + var fieldSplit: seq[string] + if field.contains(" "): + if field.contains("="): + fieldSplit = field.split("=") + elif field.contains("IS NOT"): + fieldSplit = field.split("IS NOT") + elif field.contains("IS"): + fieldSplit = field.split("IS") + elif field.contains("NOT IN"): + fieldSplit = field.split("NOT IN") + elif field.contains("IN"): + fieldSplit = field.split("IN") + elif field.contains("!="): + fieldSplit = field.split("!=") + elif field.contains("<="): + fieldSplit = field.split("<=") + elif field.contains(">="): + fieldSplit = field.split(">=") + elif field.contains("<"): + fieldSplit = field.split("<") + elif field.contains(">"): + fieldSplit = field.split(">") + else: + fieldSplit = field.split("=") + + # + # Does the data have a `=` sign? + # + if fieldSplit.len() == 2: + # + # If the data is only having equal but no value, insert a `?` sign + # + if fieldSplit[1] == "": + return (field & " ?") + # + # Otherwise just add the data as is, eg. `field = value` + # + else: + return (field) + + # + # Otherwise revert to default + # + else: + return (field & " = ?") + + + +proc hasIllegalFormats*(query: string): string = + const illegalFormats = [ + "WHERE AND", + "WHERE OR", + "AND AND", + "OR OR", + "AND OR", + "OR AND", + "WHERE IN", + "WHERE =", + "WHERE >", + "WHERE <", + "WHERE !", + "WHERE LIKE", + "WHERE NOT", + "WHERE IS", + "WHERE NULL", + "WHERE ANY" + ] + + for illegalFormat in illegalFormats: + if illegalFormat in query: + return illegalFormat + + + # + # Parentheses check + # + let + parentheseOpen = count(query, "(") + parentheseClose = count(query, ")") + + if parentheseOpen > parentheseClose: + return "parentheses does not match. Missing closing parentheses. (" & $parentheseOpen & " open, " & $parentheseClose & " close)" + elif parentheseOpen < parentheseClose: + return "parentheses does not match. Missing opening parentheses. (" & $parentheseOpen & " open, " & $parentheseClose & " close)" + + + # + # Check for double insert + # + let noSpaces = query.strip().replace(" ", "") + + if "??" in noSpaces: + return "double insert detected. (??)" + From dfc7fc065d009fc95a8b1d479a0c766244edb5a3 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:50:51 +0100 Subject: [PATCH 13/27] query: select legacy code --- src/sqlbuilderpkg/select_legacy.nim | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/sqlbuilderpkg/select_legacy.nim b/src/sqlbuilderpkg/select_legacy.nim index c57cf41..fec4983 100644 --- a/src/sqlbuilderpkg/select_legacy.nim +++ b/src/sqlbuilderpkg/select_legacy.nim @@ -1,11 +1,9 @@ # Copyright 2020 - Thomas T. Jarløv when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/macros, @@ -21,9 +19,15 @@ import proc sqlSelect*( - table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string, + table: string, + data: varargs[string], + left: varargs[string], + whereC: varargs[string], + access: string, + accessC: string, + user: string, args: ArgsContainer.query = @[], - hideIsDeleted: bool = true, + useDeleteMarker: bool = true, tablesWithDeleteMarker: varargs[string] = (when declared(tablesWithDeleteMarkerInit): tablesWithDeleteMarkerInit else: []), #@[], deleteMarker = ".is_deleted IS NULL", ): SqlQuery {.deprecated.} = @@ -84,13 +88,16 @@ proc sqlSelect*( whereInValue = @[access], customSQL = user, checkedArgs = args, - hideIsDeleted = hideIsDeleted, + useDeleteMarker = useDeleteMarker, tablesWithDeleteMarker = tablesWithDeleteMarker, deleteMarker = deleteMarker ) #[ +## +## Raw legacy procedure +## proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery = ## SQL builder for SELECT queries ## Does NOT check for NULL values @@ -193,7 +200,15 @@ proc sqlSelect*(table: string, data: varargs[string], left: varargs[string], whe result = sql(res & " FROM " & table & lef & wes & acc & " " & user) ]# -macro sqlSelectMacro*(table: string, data: varargs[string], left: varargs[string], whereC: varargs[string], access: string, accessC: string, user: string): SqlQuery {.deprecated.} = +macro sqlSelectMacro*( + table: string, + data: varargs[string], + left: varargs[string], + whereC: varargs[string], + access: string, + accessC: string, + user: string + ): SqlQuery {.deprecated.} = ## SQL builder for SELECT queries ## Does NOT check for NULL values From 31f433eecf16208c57c00fe82470437e02bc234d Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:51:04 +0100 Subject: [PATCH 14/27] query: utils --- src/sqlbuilderpkg/utils.nim | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/sqlbuilderpkg/utils.nim b/src/sqlbuilderpkg/utils.nim index 6edc40a..af6c883 100644 --- a/src/sqlbuilderpkg/utils.nim +++ b/src/sqlbuilderpkg/utils.nim @@ -21,6 +21,10 @@ type RIGHT FULL + SQLQueryType* = enum + INSERT + UPDATE + SQLJoinObject* = ref object joinType*: SQLJoinType table*: string @@ -127,7 +131,7 @@ template genArgsSetNull*[T](arguments: varargs[T, argType]): ArgsContainer = argsContainer -template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq[string], args: ArgsContainer] = +template genArgsColumns*[T](queryType: SQLQueryType, arguments: varargs[T, argFormat]): tuple[select: seq[string], args: ArgsContainer] = ## Create argument container for query and passed args and selecting which ## columns to update. It's and expansion of `genArgs()`, since you can ## decide which columns, there should be included. @@ -145,6 +149,9 @@ template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq ## columns which shall be updated. When importing your spreadsheet, check ## if the column exists (bool), and pass that as the `use: bool` param. If ## the column does not exists, it will be skipped in the query. + ## + ## The objects `ArgsFormat` is: + ## (use: bool, column: string, value: ArgObj) var select: seq[string] argsContainer: ArgsContainer @@ -154,10 +161,15 @@ template genArgsColumns*[T](arguments: varargs[T, argFormat]): tuple[select: seq let argObject = argType(arg.value) if not arg.use: continue + if ( + queryType == SQLQueryType.INSERT and + (argObject.isNull or argObject.val.len() == 0) + ): + continue if arg.column != "": select.add(arg.column) - if argObject.isNull: - argsContainer.query.add(argObject) + if argObject.isNull or argObject.val.len() == 0: + argsContainer.query.add(dbNullVal) else: argsContainer.query.add(argObject) argsContainer.args.add(argObject.val) From b70378dcadfea9a7d949b86bd6c541dba6ca5f40 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:53:04 +0100 Subject: [PATCH 15/27] query: query calls try-except --- src/sqlbuilderpkg/query_calls.nim | 118 ++++++++++++++++++------------ 1 file changed, 70 insertions(+), 48 deletions(-) diff --git a/src/sqlbuilderpkg/query_calls.nim b/src/sqlbuilderpkg/query_calls.nim index 99f4db9..b87488d 100644 --- a/src/sqlbuilderpkg/query_calls.nim +++ b/src/sqlbuilderpkg/query_calls.nim @@ -10,85 +10,107 @@ ## import - std/logging, std/typetraits +when defined(waterparkPostgres): + import waterpark/postgres +elif defined(waterparkSqlite): + import waterpark/sqlite + when NimMajor >= 2: - import - db_connector/db_common + when defined(test): + import db_connector/db_sqlite + else: + import db_connector/db_postgres else: - import - std/db_postgres + when defined(test): + import std/db_sqlite + else: + import std/db_postgres + + + +when defined(waterparkPostgres) or defined(waterparkSqlite): + discard + + + +else: + proc getValueTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = + try: + result = getValue(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc getValueTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): string = - try: - result = getValue(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + proc getAllRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[Row] = + try: + result = getAllRows(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc getAllRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): seq[Row] = - try: - result = getAllRows(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + proc getRowTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`], fillIfNull = 0): Row = + try: + result = getRow(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc getRowTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = - try: - result = getRow(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + # If the fillIfNull is set, we will fill the result with empty strings in case + # the result count does not match the fillIfNull value + if fillIfNull != 0 and result.len() != fillIfNull: + for i in 0..fillIfNull-1: + result.add("") -iterator fastRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = - try: - for i in fastRows(db, query, args): - yield i - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + iterator fastRowsTry*(db: DbConn, query: SqlQuery, args: varargs[string, `$`]): Row = + try: + for i in fastRows(db, query, args): + yield i + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc tryExecTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): bool = - try: - result = tryExec(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + proc tryExecTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): bool = + try: + result = tryExec(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc execTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]) = - try: - exec(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + proc execTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]) = + try: + exec(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) -proc execAffectedRowsTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = - try: - result = execAffectedRows(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) - return -1 + proc execAffectedRowsTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = + try: + result = execAffectedRows(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + return -1 -proc tryInsertIDTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = - try: - result = tryInsertID(db, query, args) - except: - error(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) - return -1 \ No newline at end of file + proc tryInsertIDTry*(db: DbConn, query: SqlQuery; args: varargs[string, `$`]): int64 = + try: + result = tryInsertID(db, query, args) + except: + echo(distinctBase(query).subStr(0, 200) & "\n" & getCurrentExceptionMsg()) + return -1 \ No newline at end of file From f70f20f78f1347412d66a9204b68a0ff882d16d7 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:53:48 +0100 Subject: [PATCH 16/27] package structure --- src/config.nims | 0 src/sqlbuilder_include.nim | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 src/config.nims diff --git a/src/config.nims b/src/config.nims new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlbuilder_include.nim b/src/sqlbuilder_include.nim index f8faf6d..9b6c03e 100644 --- a/src/sqlbuilder_include.nim +++ b/src/sqlbuilder_include.nim @@ -2,7 +2,8 @@ -include sqlbuilderpkg/utils +import sqlbuilderpkg/utils +export utils include sqlbuilderpkg/insert From f4494218f1c253487c7e3747bc29c29625237c07 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 10:54:03 +0100 Subject: [PATCH 17/27] legacy comment --- src/sqlbuilder_include.nim | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/sqlbuilder_include.nim b/src/sqlbuilder_include.nim index 9b6c03e..52d8b23 100644 --- a/src/sqlbuilder_include.nim +++ b/src/sqlbuilder_include.nim @@ -1,6 +1,3 @@ -# Copyright 2019 - Thomas T. Jarløv - - import sqlbuilderpkg/utils export utils From 8281a2c71080fc80ad50a02ccc43ed32367ffa1a Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 11:00:40 +0100 Subject: [PATCH 18/27] Update tests --- .gitignore | 18 +- tests/config.nims | 3 +- tests/create_db.nim | 39 ++++ tests/{ => custom_args}/test_args.nim | 20 +- tests/delete/test_delete.nim | 67 ++++++ tests/insert/test_insert.nim | 84 +++++++ tests/{ => legacy_convert}/test_legacy.nim | 96 +++++--- .../test_legacy_with_softdelete.nim | 103 ++++++--- .../test_legacy_with_softdelete2.nim | 103 +++++++++ tests/query_calls/test_query_calls.nim | 53 +++++ tests/{ => select}/test_select.nim | 66 ++---- tests/{ => select}/test_select_const.nim | 144 +++++++----- .../select/test_select_const_deletemarker.nim | 49 ++++ tests/select/test_select_deletemarker.nim | 58 +++++ tests/test2.nim | 84 ------- tests/test_insert.nim | 51 ----- tests/test_legacy_with_softdelete2.nim | 213 ------------------ tests/{ => totypes}/test_result_to_types.nim | 46 ++-- tests/{ => update}/test_update.nim | 72 +++--- tests/update/test_update_arrays.nim | 115 ++++++++++ 20 files changed, 914 insertions(+), 570 deletions(-) create mode 100644 tests/create_db.nim rename tests/{ => custom_args}/test_args.nim (77%) create mode 100644 tests/delete/test_delete.nim create mode 100644 tests/insert/test_insert.nim rename tests/{ => legacy_convert}/test_legacy.nim (74%) rename tests/{ => legacy_convert}/test_legacy_with_softdelete.nim (64%) create mode 100644 tests/legacy_convert/test_legacy_with_softdelete2.nim create mode 100644 tests/query_calls/test_query_calls.nim rename tests/{ => select}/test_select.nim (91%) rename tests/{ => select}/test_select_const.nim (84%) create mode 100644 tests/select/test_select_const_deletemarker.nim create mode 100644 tests/select/test_select_deletemarker.nim delete mode 100644 tests/test2.nim delete mode 100644 tests/test_insert.nim delete mode 100644 tests/test_legacy_with_softdelete2.nim rename tests/{ => totypes}/test_result_to_types.nim (69%) rename tests/{ => update}/test_update.nim (69%) create mode 100644 tests/update/test_update_arrays.nim diff --git a/.gitignore b/.gitignore index 0ae59ec..95c731d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,15 @@ -tests/test -tests/* -!tests/t*.nim src/sqlbuilder -.vscode \ No newline at end of file +.vscode + +tests/db_general.db +tests/mytest.db +tests/test_string + +!tests/t*.nim +tests/*/* +!tests/*/t*.nim + +!tests/config.nims +!tests/create_db.nim + +tests/test_string/test_dead.nim \ No newline at end of file diff --git a/tests/config.nims b/tests/config.nims index b0b37d7..d665900 100644 --- a/tests/config.nims +++ b/tests/config.nims @@ -1,2 +1,3 @@ switch("path", "..") -switch("d", "test") \ No newline at end of file +switch("d", "test") +switch("d", "dev") \ No newline at end of file diff --git a/tests/create_db.nim b/tests/create_db.nim new file mode 100644 index 0000000..db7365e --- /dev/null +++ b/tests/create_db.nim @@ -0,0 +1,39 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_sqlite +else: + import + std/db_sqlite + + +const dbName = "tests/db_general.db" + +proc openDB*(): DbConn = + return open(dbName, "", "", "") + +proc createDB*() = + let db = openDB() + + db.exec(sql"DROP TABLE IF EXISTS my_table") + db.exec(sql"""CREATE TABLE my_table ( + id INTEGER PRIMARY KEY, + name VARCHAR(50) NOT NULL, + age INTEGER, + ident TEXT, + is_nimmer BOOLEAN + )""") + + + db.exec(sql"INSERT INTO my_table (id, name) VALUES (0, ?)", "Jack") + + for i in 1..5: + db.exec(sql("INSERT INTO my_table (id, name, age, ident, is_nimmer) VALUES (?, ?, ?, ?, ?)"), $i, "Joe-" & $i, $i, "Nim", (if i <= 2: "true" else: "false")) + + for i in 6..10: + db.exec(sql("INSERT INTO my_table (id, name, age, ident) VALUES (?, ?, ?, ?)"), $i, "Cathrine-" & $i, $i, "Lag") + + db.close() + + diff --git a/tests/test_args.nim b/tests/custom_args/test_args.nim similarity index 77% rename from tests/test_args.nim rename to tests/custom_args/test_args.nim index 96760f9..9392f73 100644 --- a/tests/test_args.nim +++ b/tests/custom_args/test_args.nim @@ -13,25 +13,21 @@ import suite "test formats": test "genArgsColumns": - let (s, a) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + let (s, a) = genArgsColumns(SQLQueryType.INSERT, (true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - assert s == ["name", "age"] + check s == ["age"] for k, v in a.query: if k == 0: - assert $v == """(val: "", isNull: false)""" - if k == 1: - assert $v == """(val: "30", isNull: false)""" - if k == 3: - assert $v == """(val: "154", isNull: false)""" + check $v == """(val: "30", isNull: false)""" + if k == 2: + check $v == """(val: "154", isNull: false)""" for k, v in a.args: if k == 0: - assert $v == "" - if k == 1: - assert $v == "30" - if k == 3: - assert $v == "154" + check $v == "30" + if k == 2: + check $v == "154" let a1 = sqlInsert("my-table", s, a.query) let a2 = sqlDelete("my-table", s, a.query) diff --git a/tests/delete/test_delete.nim b/tests/delete/test_delete.nim new file mode 100644 index 0000000..36bffd9 --- /dev/null +++ b/tests/delete/test_delete.nim @@ -0,0 +1,67 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder, + src/sqlbuilderpkg/utils_private + + +suite "delete - normal": + + test "sqlDelete": + var test: SqlQuery + + test = sqlDelete("my-table", ["name", "age"]) + check querycompare(test, sql("DELETE FROM my-table WHERE name = ? AND age = ?")) + + + test "sqlDeleteWhere": + var test: SqlQuery + + test = sqlDelete("my-table", ["name !=", "age > "]) + check querycompare(test, sql("DELETE FROM my-table WHERE name != ? AND age > ?")) + + + test "sqlDelete with null manually": + var test: SqlQuery + + test = sqlDelete("my-table", ["name IS NULL", "age"]) + check querycompare(test, sql("DELETE FROM my-table WHERE name IS NULL AND age = ?")) + + + + +suite "delete - macro": + + test "sqlDelete": + var test: SqlQuery + + test = sqlDeleteMacro("my-table", ["name", "age"]) + check querycompare(test, sql("DELETE FROM my-table WHERE name = ? AND age = ?")) + + + test "sqlDeleteWhere": + var test: SqlQuery + + test = sqlDeleteMacro("my-table", ["name !=", "age > "]) + check querycompare(test, sql("DELETE FROM my-table WHERE name != ? AND age > ?")) + + + test "sqlDelete with null manually": + var test: SqlQuery + + test = sqlDeleteMacro("my-table", ["name IS NULL", "age"]) + check querycompare(test, sql("DELETE FROM my-table WHERE name IS NULL AND age = ?")) + + + diff --git a/tests/insert/test_insert.nim b/tests/insert/test_insert.nim new file mode 100644 index 0000000..06fc2b1 --- /dev/null +++ b/tests/insert/test_insert.nim @@ -0,0 +1,84 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilder, + src/sqlbuilderpkg/utils_private + + + +suite "insert - custom args": + + test "sqlInsert - dynamic columns": + var test: SqlQuery + + let (s, a1) = genArgsColumns(SQLQueryType.INSERT, (true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + test = sqlInsert("my-table", s, a1.query) + check querycompare(test, sql("INSERT INTO my-table (age) VALUES (?)")) + + + test "sqlInsert - setting null": + var test: SqlQuery + + let a2 = genArgsSetNull("hje", "") + test = sqlInsert("my-table", ["name", "age"], a2.query) + check querycompare(test, sql("INSERT INTO my-table (name) VALUES (?)")) + + + +suite "insert - default": + + test "sqlInsert - default": + var test: SqlQuery + + test = sqlInsert("my-table", ["name", "age"]) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) + + + test "sqlInsert - with manual null": + var test: SqlQuery + + test = sqlInsert("my-table", ["name", "age = NULL"]) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) + + + test "sqlInsert - with args check for null #1": + var test: SqlQuery + + let vals = @["thomas", "30"] + test = sqlInsert("my-table", ["name", "age"], vals) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) + + + test "sqlInsert - with args check for null #2": + var test: SqlQuery + + let vals = @["thomas", ""] + test = sqlInsert("my-table", ["name", "age"], vals) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) + + +suite "insert - macro": + + test "sqlInsert - default": + var test: SqlQuery + + test = sqlInsertMacro("my-table", ["name", "age"]) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) + + + test "sqlInsertMacro - with manual null": + var test: SqlQuery + + test = sqlInsertMacro("my-table", ["name", "age = NULL"]) + check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) diff --git a/tests/test_legacy.nim b/tests/legacy_convert/test_legacy.nim similarity index 74% rename from tests/test_legacy.nim rename to tests/legacy_convert/test_legacy.nim index 4e80476..ecfb819 100644 --- a/tests/test_legacy.nim +++ b/tests/legacy_convert/test_legacy.nim @@ -1,39 +1,17 @@ # Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/strutils, std/unittest import - src/sqlbuilder - - - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - + src/sqlbuilder, + src/sqlbuilderpkg/utils_private @@ -165,6 +143,72 @@ suite "legacy - sqlSelect(Convert)": """)) + test "complex query": + + var test: SqlQuery + + test = sqlSelect("tasksitems AS tasks", + [ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + [ + "history AS his ON his.id = tasks.hid AND his.status = 1", + "projects ON projects.id = tasks.project_id AND projects.status = 1", + "person ON person.id = tasks.person_id" + ], + [ + "projects.id =", + "tasks.status >" + ], + "1,2,3", + "tasks.id", + "ORDER BY tasks.created DESC" + ) + + check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + ORDER BY + tasks.created DESC + """))) + + suite "legacy - sqlSelect(Convert) - genArgs": diff --git a/tests/test_legacy_with_softdelete.nim b/tests/legacy_convert/test_legacy_with_softdelete.nim similarity index 64% rename from tests/test_legacy_with_softdelete.nim rename to tests/legacy_convert/test_legacy_with_softdelete.nim index 6731dea..048d775 100644 --- a/tests/test_legacy_with_softdelete.nim +++ b/tests/legacy_convert/test_legacy_with_softdelete.nim @@ -1,43 +1,26 @@ # Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/strutils, std/unittest -# import -# src/sqlbuilder +import + src/sqlbuilderpkg/utils_private + const tablesWithDeleteMarkerInit* = ["tasks", "history", "tasksitems"] include src/sqlbuilder_include -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - - +# +# Differs by using less fields in `tablesWithDeleteMarkerInit` +# suite "legacy - sqlSelect(converter) - with new functionality to avoid regression": @@ -63,7 +46,7 @@ suite "legacy - sqlSelect(converter) - with new functionality to avoid regressio """)) - test "existing delete in where": + test "existing is_deleted in where": let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["project AS p ON p.id = t.project_id"], ["t.id ="], "2,4,6,7", "p.id", "AND tasks.is_deleted IS NULL ORDER BY t.name") @@ -153,3 +136,71 @@ suite "legacy - sqlSelect(converter) - with new functionality to avoid regressio ORDER BY t.name """)) + + + test "complex query": + + var test: SqlQuery + + test = sqlSelect("tasksitems AS tasks", + [ + "tasks.id", + "tasks.name", + "tasks.status", + "tasks.created", + "his.id", + "his.name", + "his.status", + "his.created", + "projects.id", + "projects.name", + "person.id", + "person.name", + "person.email" + ], + [ + "history AS his ON his.id = tasks.hid AND his.status = 1", + "projects ON projects.id = tasks.project_id AND projects.status = 1", + "person ON person.id = tasks.person_id" + ], + [ + "projects.id =", + "tasks.status >" + ], + "1,2,3", + "tasks.id", + "ORDER BY tasks.created DESC" + ) + + check querycompare(test, (sql(""" + SELECT + tasks.id, + tasks.name, + tasks.status, + tasks.created, + his.id, + his.name, + his.status, + his.created, + projects.id, + projects.name, + person.id, + person.name, + person.email + FROM + tasksitems AS tasks + LEFT JOIN history AS his ON + (his.id = tasks.hid AND his.status = 1) + LEFT JOIN projects ON + (projects.id = tasks.project_id AND projects.status = 1) + LEFT JOIN person ON + (person.id = tasks.person_id) + WHERE + projects.id = ? + AND tasks.status > ? + AND tasks.id in (1,2,3) + AND tasks.is_deleted IS NULL + AND his.is_deleted IS NULL + ORDER BY + tasks.created DESC + """))) diff --git a/tests/legacy_convert/test_legacy_with_softdelete2.nim b/tests/legacy_convert/test_legacy_with_softdelete2.nim new file mode 100644 index 0000000..2fa1f9f --- /dev/null +++ b/tests/legacy_convert/test_legacy_with_softdelete2.nim @@ -0,0 +1,103 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import + db_connector/db_common +else: + import + std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilderpkg/utils_private + + +const tablesWithDeleteMarkerInit* = ["tasks", "history", "tasksitems", "persons", "actions", "project"] +include + src/sqlbuilder_include + + + + +# +# Differs by using more fields in `tablesWithDeleteMarkerInit` +# +suite "legacy - sqlSelect(converter) - with new functionality to avoid regression - #2": + + + test "existing delete in left join (double) - delete marker from left join": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN invoice AS p ON + (p.id = t.invoice_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id AND persons.is_deleted IS NULL) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "add delete marker in left join - delete marker from left join": + + let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + p.id + FROM + tasks AS t + LEFT JOIN invoice AS p ON + (p.id = t.invoice_id) + LEFT JOIN persons ON + (persons.id = tasks.person_id) + WHERE + t.id = ? + AND p.id in (2,4,6,7) + AND t.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + + test "set left join without AS": + + let test = sqlSelect("tasks", ["t.id", "t.name", "invoice.id"], ["persons ON persons.id = t.persons_id"], ["t.id ="], "2,4,6,7", "invoice.id", "ORDER BY t.name") + + check querycompare(test, sql(""" + SELECT + t.id, + t.name, + invoice.id + FROM + tasks + LEFT JOIN persons ON + (persons.id = t.persons_id) + WHERE + t.id = ? + AND invoice.id in (2,4,6,7) + AND tasks.is_deleted IS NULL + AND persons.is_deleted IS NULL + ORDER BY + t.name + """)) + + diff --git a/tests/query_calls/test_query_calls.nim b/tests/query_calls/test_query_calls.nim new file mode 100644 index 0000000..2cefa39 --- /dev/null +++ b/tests/query_calls/test_query_calls.nim @@ -0,0 +1,53 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + + +when NimMajor >= 2: + import db_connector/db_sqlite +else: + import std/db_sqlite + +import + std/logging, + std/strutils, + std/unittest + +import + src/sqlbuilderpkg/query_calls + +import + tests/create_db + + +var consoleLog = newConsoleLogger() +addHandler(consoleLog) + + + +# +# Set up a test database +# +createDB() +let db = openDB() + +for i in 1..5: + db.exec(sql("INSERT INTO my_table (name, age, ident, is_nimmer) VALUES (?, ?, ?, ?)"), "Call-" & $i, $i, "Call", (if i <= 2: "true" else: "false")) + + + +suite "Query calls": + + test "getRowTry() - with match": + let row = getRowTry(db, sql("SELECT name, age, ident, is_nimmer FROM my_table WHERE name = ?"), ["Call-1"]) + check row == @["Call-1", "1", "Call", "true"] + + test "getRowTry() - without match": + let row = getRowTry(db, sql("SELECT name, age, ident, is_nimmer FROM my_table WHERE name = ?"), ["Call-"]) + check row == @["", "", "", ""] + + test "getRowTry() - missing args": + let row = getRowTry(db, sql("SELECT name, age, ident, is_nimmer FROM my_table WHERE name = ?"), []) + check row.len() == 0 + + test "getRowTry() - missing args auto fill": + let row = getRowTry(db, sql("SELECT name, age, ident, is_nimmer FROM my_table WHERE name = ?"), [], fillIfNull = 4) + check row == @["", "", "", ""] \ No newline at end of file diff --git a/tests/test_select.nim b/tests/select/test_select.nim similarity index 91% rename from tests/test_select.nim rename to tests/select/test_select.nim index fe814f0..858eea2 100644 --- a/tests/test_select.nim +++ b/tests/select/test_select.nim @@ -1,37 +1,17 @@ # Copyright Thomas T. Jarløv (TTJ) when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/strutils, std/unittest import - src/sqlbuilder - - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - + src/sqlbuilder, + src/sqlbuilderpkg/utils_private @@ -39,14 +19,14 @@ proc querycompare(a, b: SqlQuery): bool = suite "test sqlSelect": - test "hideIsDeleted = false": + test "useDeleteMarker = false": var test: SqlQuery test = sqlSelect( table = "tasks", select = @["id", "name", "description", "created", "updated", "completed"], where = @["id ="], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) @@ -59,7 +39,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "name", "description", "created", "updated", "completed"], where = @["id ="], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) @@ -69,7 +49,7 @@ suite "test sqlSelect": tableAs = "t", select = @["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = @["t.id ="], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t WHERE t.id = ? ")) @@ -82,7 +62,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "name", "description", "created", "updated", "completed"], where = @["id =", "name !=", "updated >", "completed IS", "description LIKE"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? ")) check string(test).count("?") == 5 @@ -94,7 +74,7 @@ suite "test sqlSelect": select = @["id", "name", "description", "created", "updated", "completed"], where = @["id =", "name !=", "updated >", "completed IS", "description LIKE"], customSQL = "AND name != 'test' AND created > ? ", - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? AND name != 'test' AND created > ? ")) check string(test).count("?") == 6 @@ -109,7 +89,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "ids_array"], where = @["id =", "= ANY(ids_array)"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) ")) check string(test).count("?") == 2 @@ -124,7 +104,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "ids_array"], where = @["id =", "IN (ids_array)"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? IN (ids_array) ")) check string(test).count("?") == 2 @@ -135,7 +115,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "ids_array"], where = @["id =", "id IN"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND id IN (?) ")) check string(test).count("?") == 2 @@ -150,7 +130,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "name", "description", "created", "updated", "completed"], where = @["id =", "name != NULL", "description = NULL"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) check string(test).count("?") == 1 @@ -161,7 +141,7 @@ suite "test sqlSelect": tableAs = "t", select = @["id", "name", "description", "created", "updated", "completed"], where = @["id =", "name !=", "description = NULL"], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND description = NULL ")) check string(test).count("?") == 2 @@ -179,7 +159,7 @@ suite "test sqlSelect - joins": select = @["id", "name"], where = @["id ="], joinargs = @[(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) @@ -192,7 +172,7 @@ suite "test sqlSelect - joins": select = @["id", "name"], where = @["id ="], joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) @@ -207,7 +187,7 @@ suite "test sqlSelect - joins": where = @["id ="], joinargs = @[(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], jointype = INNER, - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) @@ -286,13 +266,13 @@ suite "test sqlSelect - deletemarkers / softdelete": test "complex query": var test: SqlQuery - let tableWithDeleteMarkerLet = @["tasks", "history", "tasksitems"] + let tableWithDeleteMarkerLet = @["history", "tasksitems"] test = sqlSelect( table = "tasksitems", tableAs = "tasks", - select = @[ + select = [ "tasks.id", "tasks.name", "tasks.status", @@ -307,17 +287,17 @@ suite "test sqlSelect - deletemarkers / softdelete": "person.name", "person.email" ], - where = @[ + where = [ "projects.id =", "tasks.status >" ], - joinargs = @[ + joinargs = [ (table: "history", tableAs: "his", on: @["his.id = tasks.hid", "his.status = 1"]), (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), (table: "person", tableAs: "", on: @["person.id = tasks.person_id"]) ], whereInField = "tasks.id", - whereInValue = @["1", "2", "3"], + whereInValue = ["1", "2", "3"], customSQL = "ORDER BY tasks.created DESC", tablesWithDeleteMarker = tableWithDeleteMarkerLet ) diff --git a/tests/test_select_const.nim b/tests/select/test_select_const.nim similarity index 84% rename from tests/test_select_const.nim rename to tests/select/test_select_const.nim index f7df106..396538d 100644 --- a/tests/test_select_const.nim +++ b/tests/select/test_select_const.nim @@ -1,50 +1,33 @@ # Copyright Thomas T. Jarløv (TTJ) when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import std/strutils, std/unittest import - src/sqlbuilder + src/sqlbuilder, + src/sqlbuilderpkg/utils_private -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - suite "test sqlSelectConst": - test "hideIsDeleted = false": + test "useDeleteMarker = false": var test: SqlQuery test = sqlSelectConst( table = "tasks", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) @@ -58,7 +41,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) @@ -68,7 +52,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = ["t.id ="], - hideIsDeleted = false, + joinargs = [noJoin], + useDeleteMarker = false, customSQL = "ORDER BY t.created DESC" ) check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t WHERE t.id = ? ORDER BY t.created DESC ")) @@ -83,7 +68,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? ")) check string(test).count("?") == 5 @@ -94,8 +80,9 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], + joinargs = [noJoin], customSQL = "AND name != 'test' AND created > ? ", - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? AND name != 'test' AND created > ? ")) check string(test).count("?") == 6 @@ -110,7 +97,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "= ANY(ids_array)"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) ")) check string(test).count("?") == 2 @@ -125,7 +113,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "= ANY(ids_array)", "= ANY(user_array)", "= ANY(tasks_array)"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) AND ? = ANY(user_array) AND ? = ANY(tasks_array) ")) check string(test).count("?") == 4 @@ -140,7 +129,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "IN (ids_array)"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? IN (ids_array) ")) check string(test).count("?") == 2 @@ -151,7 +141,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "id IN"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND id IN (?) ")) check string(test).count("?") == 2 @@ -166,7 +157,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name != NULL", "description = NULL"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) check string(test).count("?") == 1 @@ -177,7 +169,8 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "description = NULL"], - hideIsDeleted = false + joinargs = [noJoin], + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND description = NULL ")) check string(test).count("?") == 2 @@ -196,7 +189,7 @@ suite "test sqlSelectConst - joins": select = ["id", "name"], where = ["id ="], joinargs = [(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) @@ -209,7 +202,7 @@ suite "test sqlSelectConst - joins": select = ["id", "name"], where = ["id ="], joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) @@ -222,7 +215,7 @@ suite "test sqlSelectConst - joins": select = ["id", "name"], where = ["id ="], joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), (table: "invoice", tableAs: "", on: @["invoice.id = t.invoice_id", "invoice.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) LEFT JOIN invoice ON (invoice.id = t.invoice_id AND invoice.status = 1) WHERE id = ? ")) @@ -235,7 +228,7 @@ suite "test sqlSelectConst - joins": select = ["id", "name"], where = ["id ="], joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), (table: "invoice", tableAs: "", on: @["invoice.id = t.invoice_id", "invoice.status = 1"]), (table: "letter", tableAs: "", on: @["letter.id = t.letter_id", "letter.status = 1"])], - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) LEFT JOIN invoice ON (invoice.id = t.invoice_id AND invoice.status = 1) LEFT JOIN letter ON (letter.id = t.letter_id AND letter.status = 1) WHERE id = ? ")) @@ -250,11 +243,61 @@ suite "test sqlSelectConst - joins": where = ["id ="], joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], jointype = INNER, - hideIsDeleted = false + useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + test "JOIN #1": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + useDeleteMarker = false, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + + + test "JOIN #2": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + joinargs = [(table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"])], + useDeleteMarker = false, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE t.id = ? ")) + + + test "JOIN #3": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + tableAs = "t", + select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], + where = ["t.id ="], + # joinargs = [noJoin], + joinargs = [ + (table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), + (table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]) + ], + useDeleteMarker = false, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + ) + check querycompare(test, sql("SELECT t.id, t.name, t.description, t.created, t.updated, t.completed FROM tasks AS t LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) LEFT JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE t.id = ? ")) + + + suite "test sqlSelectConst - deletemarkers / softdelete": @@ -295,7 +338,6 @@ suite "test sqlSelectConst - deletemarkers / softdelete": test "deletemarkers from const": - # var test: SqlQuery const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] let test = sqlSelectConst( @@ -303,8 +345,8 @@ suite "test sqlSelectConst - deletemarkers / softdelete": select = ["id", "name"], where = ["id ="], joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet, - # tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] + # tablesWithDeleteMarker = tableWithDeleteMarkerLet, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] ) check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN projects ON (projects.id = tasks.project_id AND projects.status = 1) WHERE id = ? AND tasks.is_deleted IS NULL ")) @@ -336,6 +378,7 @@ suite "test sqlSelectConst - deletemarkers / softdelete": table = "tasks", select = ["id", "name"], where = ["id ="], + joinargs = [noJoin], # joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], # tablesWithDeleteMarker = [] #tableWithDeleteMarkerLet, tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] @@ -364,14 +407,14 @@ suite "test sqlSelectConst - deletemarkers / softdelete": test "deletemarkers misc": var test: SqlQuery - const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] + # const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] test = sqlSelectConst( table = "tasks", select = ["id", "name"], where = ["id ="], joinargs = [(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] ) check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.is_deleted IS NULL AND history.is_deleted IS NULL ")) @@ -382,7 +425,7 @@ suite "test sqlSelectConst - deletemarkers / softdelete": select = ["tasks.id", "tasks.name"], where = ["tasks.id ="], joinargs = [(table: "history", tableAs: "his", on: @["his.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] ) check querycompare(test, sql("SELECT tasks.id, tasks.name FROM tasks LEFT JOIN history AS his ON (his.id = tasks.hid) WHERE tasks.id = ? AND tasks.is_deleted IS NULL AND his.is_deleted IS NULL ")) @@ -404,14 +447,13 @@ suite "test sqlSelectConst - deletemarkers / softdelete": test "custom deletemarker override": var test: SqlQuery - const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] test = sqlSelectConst( table = "tasks", select = ["id", "name"], where = ["id ="], joinargs = [(table: "history", tableAs: "", on: @["history.id = tasks.hid"])], - tablesWithDeleteMarker = tableWithDeleteMarkerLet, + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], deleteMarker = ".deleted_at = 543234563" ) check querycompare(test, sql("SELECT id, name FROM tasks LEFT JOIN history ON (history.id = tasks.hid) WHERE id = ? AND tasks.deleted_at = 543234563 AND history.deleted_at = 543234563 ")) @@ -421,9 +463,6 @@ suite "test sqlSelectConst - deletemarkers / softdelete": test "complex query": var test: SqlQuery - const tableWithDeleteMarkerLet = ["tasks", "history", "tasksitems"] - - test = sqlSelectConst( table = "tasksitems", tableAs = "tasks", @@ -454,7 +493,7 @@ suite "test sqlSelectConst - deletemarkers / softdelete": whereInField = "tasks.id", whereInValue = ["1", "2", "3"], customSQL = "ORDER BY tasks.created DESC", - tablesWithDeleteMarker = tableWithDeleteMarkerLet + tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] ) check querycompare(test, (sql(""" SELECT @@ -502,6 +541,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = ["t.id ="], + joinargs = [noJoin], tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker ) @@ -611,6 +651,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name"], where = ["t.id ="], + joinargs = [noJoin], whereInField = "t.name", whereInValue = ["'1aa'", "'2bb'", "'3cc'"], tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker @@ -632,6 +673,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name"], where = ["t.id ="], + joinargs = [noJoin], whereInField = "t.id", whereInValue = [""], tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker diff --git a/tests/select/test_select_const_deletemarker.nim b/tests/select/test_select_const_deletemarker.nim new file mode 100644 index 0000000..66f85e0 --- /dev/null +++ b/tests/select/test_select_const_deletemarker.nim @@ -0,0 +1,49 @@ + +# +# This sets a global delete marker +# + + +when NimMajor >= 2: + import db_connector/db_common +else: + import std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilderpkg/utils_private + +const tablesWithDeleteMarkerInit = ["tasks"] + +include src/sqlbuilder_include + + +suite "select with tablesWithDeleteMarkerInit init": + + test "constants": + var test: SqlQuery + + test = sqlSelectConst( + table = "tasks", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id ="], + joinargs = [noJoin], + useDeleteMarker = true + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + test "dynamic": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = ["id", "name", "description", "created", "updated", "completed"], + where = ["id ="], + joinargs = [noJoin], + useDeleteMarker = true + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) diff --git a/tests/select/test_select_deletemarker.nim b/tests/select/test_select_deletemarker.nim new file mode 100644 index 0000000..765b0b6 --- /dev/null +++ b/tests/select/test_select_deletemarker.nim @@ -0,0 +1,58 @@ +# Copyright Thomas T. Jarløv (TTJ) + +when NimMajor >= 2: + import db_connector/db_common +else: + import std/db_common + +import + std/strutils, + std/unittest + +import + src/sqlbuilderpkg/utils_private + + +const tablesWithDeleteMarkerInit = ["tasks"] + +include src/sqlbuilder_include + + +suite "sqlSelect - delete marker const": + + test "useDeleteMarker = default": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + # useDeleteMarker = true + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + test "useDeleteMarker = true": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + useDeleteMarker = true + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) + + + test "useDeleteMarker = false": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id ="], + useDeleteMarker = false + ) + check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) + + diff --git a/tests/test2.nim b/tests/test2.nim deleted file mode 100644 index ff95e85..0000000 --- a/tests/test2.nim +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk - - - -# import src/sqlbuilderpkg/insert -# export insert - -# import src/sqlbuilderpkg/update -# export update - -# import src/sqlbuilderpkg/delete -# export delete - -# import src/sqlbuilderpkg/utils -# export utils - -import std/unittest -const tablesWithDeleteMarkerInit = ["tasks", "persons"] - -include src/sqlbuilder_include - - - - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - - -suite "test sqlSelect": - - test "set tablesWithDeleteMarker": - - let test = sqlSelect( - table = "tasks", - select = @["id", "name", "description", "created", "updated", "completed"], - where = @["id ="], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) - - - -suite "test sqlSelect with inline NULL": - - test "set tablesWithDeleteMarker": - - let test = sqlSelect( - table = "tasks", - select = @["id", "name", "description", "created", "updated", "completed"], - where = @["id =", "name = NULL"], - hideIsDeleted = false - ) - check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND name = NULL ")) - - - -suite "test sqlSelectConst": - - test "set tablesWithDeleteMarker": - - let a = sqlSelectConst( - table = "tasks", - tableAs = "t", - select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], - # joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"]), (table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], - where = ["t.id ="], - tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker - ) - diff --git a/tests/test_insert.nim b/tests/test_insert.nim deleted file mode 100644 index fc5b76b..0000000 --- a/tests/test_insert.nim +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk - -when NimMajor >= 2: - import - db_connector/db_common -else: - import - std/db_common - -import - std/strutils, - std/unittest - -import - src/sqlbuilder - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - -suite "insert": - - test "sqlInsert - dynamic columns": - var test: SqlQuery - - let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - test = sqlInsert("my-table", s, a1.query) - check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) - - - test "sqlInsert - setting null": - var test: SqlQuery - - let a2 = genArgsSetNull("hje", "") - test = sqlInsert("my-table", ["name", "age"], a2.query) - # discard tryInsertID(sqlInsert("my-table", ["name", "age"], a2.query), a2.args) - check querycompare(test, sql("INSERT INTO my-table (name) VALUES (?)")) \ No newline at end of file diff --git a/tests/test_legacy_with_softdelete2.nim b/tests/test_legacy_with_softdelete2.nim deleted file mode 100644 index 1584f69..0000000 --- a/tests/test_legacy_with_softdelete2.nim +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk - -when NimMajor >= 2: - import - db_connector/db_common -else: - import - std/db_common - -import - std/strutils, - std/unittest - -# import -# src/sqlbuilder - -const tablesWithDeleteMarkerInit* = ["tasks", "history", "tasksitems", "persons", "actions", "project"] -include - src/sqlbuilder_include - - -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" - - return a1 == b1 - - - -suite "legacy - sqlSelect(converter) - with new functionality to avoid regression - #2": - - - test "existing delete in left join (double) - delete marker from left join": - - let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id AND persons.is_deleted IS NULL"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN invoice AS p ON - (p.id = t.invoice_id) - LEFT JOIN persons ON - (persons.id = tasks.person_id AND persons.is_deleted IS NULL) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - AND t.is_deleted IS NULL - AND persons.is_deleted IS NULL - ORDER BY - t.name - """)) - - - test "add delete marker in left join - delete marker from left join": - - let test = sqlSelect("tasks AS t", ["t.id", "t.name", "p.id"], ["invoice AS p ON p.id = t.invoice_id", "persons ON persons.id = tasks.person_id"], ["t.id ="], "2,4,6,7", "p.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - p.id - FROM - tasks AS t - LEFT JOIN invoice AS p ON - (p.id = t.invoice_id) - LEFT JOIN persons ON - (persons.id = tasks.person_id) - WHERE - t.id = ? - AND p.id in (2,4,6,7) - AND t.is_deleted IS NULL - AND persons.is_deleted IS NULL - ORDER BY - t.name - """)) - - - - - - test "set left join without AS": - - let test = sqlSelect("tasks", ["t.id", "t.name", "invoice.id"], ["persons ON persons.id = t.persons_id"], ["t.id ="], "2,4,6,7", "invoice.id", "ORDER BY t.name") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - invoice.id - FROM - tasks - LEFT JOIN persons ON - (persons.id = t.persons_id) - WHERE - t.id = ? - AND invoice.id in (2,4,6,7) - AND tasks.is_deleted IS NULL - AND persons.is_deleted IS NULL - ORDER BY - t.name - """)) - - - - -#[ - test "set left join without AS": - - let test = sqlSelect("tasks", - [ - "DISTINCT actions.virtual_id", #0 - "actions.color", - "status_project.name", - "swimlanes_project.name", - "actions.name", - "phases_project.name", - "categories_project.name", - "actions.tags", - "actions.date_start", - "actions.date_end", - "actions.disp1", #10 - "actions.status", - "actions.modified", - "actions.description", - "actions.assigned_to", - "actions.dependenciesb", - "status_project.closed", - "(SELECT coalesce(string_agg(history.user_id::text, ','), '') FROM history WHERE history.item_id = actions.virtual_id AND history.project_id = actions.project_id AND (history.choice = 'Comment' OR history.choice = 'Picture' OR history.choice = 'File') AND history.is_deleted IS NULL) AS history", - "(SELECT COUNT(files_tasks.id) FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND (files_tasks.filetype IS NULL OR files_tasks.filetype != 1)) AS files", - "actions.floorplanJson", - "actions.canBeClosed", #20 - "personModified.uuid", - "actions.qa_type", - "personAssigned.uuid", - "personModified.name", - "cA.name", - "cA.logo", - "actions.assigned_to_company", - "actions.cx_review_document", - "actions.cx_review_page", - "actions.cx_bod", #30 - "actions.project_id", #31 - "actions.rand", #32 - "personAuthor.uuid", #33 - "personAuthor.name", #34 - "personAssigned.hasPicture", #35 - "personModified.hasPicture", #36 - "qap.virtual_id", #37 - "qap.name", #38 - "actions.cost", #39 - "personAuthor.hasPicture", #40 - "actions.creation", #41 - "floorplan.filename", #42 - "personAuthor.company", #43 - "(SELECT files_tasks.filename_hash FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND (files_tasks.filetype IS NULL OR files_tasks.filetype != 1) ORDER BY files_tasks.id DESC LIMIT 1) AS filesPhoto", - "(SELECT files_tasks.filename_hash FROM files_tasks WHERE files_tasks.project_id = actions.project_id AND files_tasks.task_id = actions.virtual_id AND files_tasks.filetype = 1 ORDER BY files_tasks.id DESC LIMIT 1) AS filesFloorplan", - "(SELECT SUBSTRING(history.text, 0, 100) FROM history WHERE history.project_id = actions.project_id AND history.item_id = actions.virtual_id AND history.choice = 'Comment' AND history.is_deleted IS NULL ORDER BY history.id DESC LIMIT 1) AS lastComment", - "actions.location", #47 - "actions.customfields", #48 - "actions.testvalue", #49 - "actions.added_closed", #50 - #"personAuthor.uuid", # - ], - [ - "categories_project ON actions.category = categories_project.id", - "person as personAuthor ON actions.author_id = personAuthor.id", - "person as personModified ON actions.modifiedBy = personModified.id", - "person as personAssigned ON actions.assigned_to_userid = personAssigned.id", - "status_project ON actions.status = status_project.status AND actions.project_id = status_project.project_id AND actions.swimlane = status_project.swimlane_id", - "swimlanes_project on actions.swimlane = swimlanes_project.id", - "phases_project ON actions.phase = phases_project.id", - "project ON actions.project_id = project.id", - "company AS cA ON cA.id = actions.assigned_to_companyid", - "qa_paradigm AS qap ON qap.project_id = actions.project_id AND qap.id = actions.qa_id", - "files AS floorplan ON floorplan.project_id = actions.project_id AND floorplan.filename_hash = actions.floorplanhash" # This forces us to use DISTINCT on actions.id - ], ["actions.project_id ="], "", "", "ORDER BY status_project.closed ASC, actions.modified DESC LIMIT 500 OFFSET 0") - - check querycompare(test, sql(""" - SELECT - t.id, - t.name, - project.id - FROM - tasks - LEFT JOIN persons ON - (persons.id = t.persons_id) - WHERE - t.id = ? - AND project.id in (2,4,6,7) - AND tasks.is_deleted IS NULL - AND persons.is_deleted IS NULL - ORDER BY - t.name - """)) - - -]# \ No newline at end of file diff --git a/tests/test_result_to_types.nim b/tests/totypes/test_result_to_types.nim similarity index 69% rename from tests/test_result_to_types.nim rename to tests/totypes/test_result_to_types.nim index d2a58a9..cf1a923 100644 --- a/tests/test_result_to_types.nim +++ b/tests/totypes/test_result_to_types.nim @@ -5,11 +5,9 @@ import std/unittest when NimMajor >= 2: - import - db_connector/db_sqlite + import db_connector/db_sqlite else: - import - std/db_sqlite + import std/db_sqlite import std/strutils @@ -18,6 +16,8 @@ import src/sqlbuilderpkg/select, src/sqlbuilderpkg/totypes +import + tests/create_db type Person = ref object @@ -30,24 +30,26 @@ type # # Set up a test database # -let db = open("tests/mytest.db", "", "", "") - -db.exec(sql"DROP TABLE IF EXISTS my_table") -db.exec(sql"""CREATE TABLE my_table ( - id INTEGER, - name VARCHAR(50) NOT NULL, - age INTEGER, - ident TEXT, - is_nimmer BOOLEAN - )""") - -db.exec(sql"INSERT INTO my_table (id, name) VALUES (0, ?)", "Jack") - -for i in 1..5: - db.exec(sql("INSERT INTO my_table (id, name, age, ident, is_nimmer) VALUES (?, ?, ?, ?, ?)"), $i, "Joe-" & $i, $i, "Nim", (if i <= 2: "true" else: "false")) - -for i in 6..10: - db.exec(sql("INSERT INTO my_table (id, name, age, ident) VALUES (?, ?, ?, ?)"), $i, "Cathrine-" & $i, $i, "Lag") +createDB() +let db = openDB() +# let db = open("tests/db_types.db", "", "", "") + +# db.exec(sql"DROP TABLE IF EXISTS my_table") +# db.exec(sql"""CREATE TABLE my_table ( +# id INTEGER, +# name VARCHAR(50) NOT NULL, +# age INTEGER, +# ident TEXT, +# is_nimmer BOOLEAN +# )""") + +# db.exec(sql"INSERT INTO my_table (id, name) VALUES (0, ?)", "Jack") + +# for i in 1..5: +# db.exec(sql("INSERT INTO my_table (id, name, age, ident, is_nimmer) VALUES (?, ?, ?, ?, ?)"), $i, "Joe-" & $i, $i, "Nim", (if i <= 2: "true" else: "false")) + +# for i in 6..10: +# db.exec(sql("INSERT INTO my_table (id, name, age, ident) VALUES (?, ?, ?, ?)"), $i, "Cathrine-" & $i, $i, "Lag") diff --git a/tests/test_update.nim b/tests/update/test_update.nim similarity index 69% rename from tests/test_update.nim rename to tests/update/test_update.nim index a9f71f5..b563484 100644 --- a/tests/test_update.nim +++ b/tests/update/test_update.nim @@ -1,61 +1,58 @@ # Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk when NimMajor >= 2: - import - db_connector/db_common + import db_connector/db_common else: - import - std/db_common + import std/db_common import - std/strutils, std/unittest import - src/sqlbuilder + src/sqlbuilder, + src/sqlbuilderpkg/utils_private -proc querycompare(a, b: SqlQuery): bool = - var - a1: seq[string] - b1: seq[string] - for c in splitWhitespace(string(a)): - a1.add($c) - for c in splitWhitespace(string(b)): - b1.add($c) - if a1 != b1: - echo "" - echo "a1: ", string(a) - echo "b1: ", string(b).replace("\n", " ").splitWhitespace().join(" ") - echo "" +suite "update - custom args": - return a1 == b1 + test "sqlUpdate using genArgs #1": + let a3 = genArgs("hje", "") + let q = sqlUpdate("my-table", ["name", "age"], ["id"], a3.query) + check querycompare(q, sql("UPDATE my-table SET name = ?, age = ? WHERE id = ?")) + test "sqlUpdate using genArgs #2": + let a4 = genArgs("hje", dbNullVal) + let q2 = sqlUpdate("my-table", ["name", "age"], ["id"], a4.query) + check querycompare(q2, sql("UPDATE my-table SET name = ?, age = NULL WHERE id = ?")) + test "sqlUpdate using genArgsColumns #1": + let (s, a1) = genArgsColumns(SQLQueryType.UPDATE, (true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + let q = sqlUpdate("my-table", s, ["id"], a1.query) + check querycompare(q, sql("UPDATE my-table SET name = NULL, age = ? WHERE id = ?")) -suite "update": + test "sqlUpdate using genArgsColumns #2 - empty string IS NULL (nim-field)": + let (s, a1) = genArgsColumns(SQLQueryType.UPDATE, (true, "name", ""), (true, "age", 30), (true, "nim", ""), (true, "", "154")) + let q = sqlUpdate("my-table", s, ["id"], a1.query) + check querycompare(q, sql("UPDATE my-table SET name = NULL, age = ?, nim = NULL WHERE id = ?")) - test "sqlUpdate using genArgs": - let a3 = genArgs("hje", "") - discard sqlUpdate("my-table", ["name", "age"], ["id"], a3.query) - assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" - - let a4 = genArgs("hje", dbNullVal) - discard sqlUpdate("my-table", ["name", "age"], ["id"], a4.query) - assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" + test "sqlUpdate using genArgsColumns #2 - empty string is ignored (nim-field)": + let (s, a1) = genArgsColumns(SQLQueryType.UPDATE, (true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) + let q = sqlUpdate("my-table", s, ["id"], a1.query) + check querycompare(q, sql("UPDATE my-table SET name = NULL, age = ? WHERE id = ?")) - test "sqlUpdate using genArgsColumns": - let (s, a1) = genArgsColumns((true, "name", ""), (true, "age", 30), (false, "nim", ""), (true, "", "154")) - discard sqlUpdate("my-table", s, ["id"], a1.query) - assert testout == "UPDATE my-table SET name = ?, age = ? WHERE id = ?" + test "sqlUpdate using genArgsColumns #3": + let (s, a1) = genArgsColumns(SQLQueryType.UPDATE, (true, "name", ""), (false, "age", 30), (false, "nim", ""), (true, "", "154")) + let q = sqlUpdate("my-table", s, ["id"], a1.query) + check querycompare(q, sql("UPDATE my-table SET name = NULL WHERE id = ?")) test "sqlUpdate using genArgsSetNull": let a2 = genArgsSetNull("hje", "") - discard sqlUpdate("my-table", ["name", "age"], ["id"], a2.query) - assert testout == "UPDATE my-table SET name = ?, age = NULL WHERE id = ?" + let q = sqlUpdate("my-table", ["name", "age"], ["id"], a2.query) + check querycompare(q, sql("UPDATE my-table SET name = ?, age = NULL WHERE id = ?")) +suite "update - queries": test "update value": let q = sqlUpdate( @@ -130,7 +127,6 @@ suite "update": suite "update macro": test "update value": - let q = sqlUpdateMacro( "table", ["name", "age", "info"], @@ -204,4 +200,6 @@ suite "update macro": ["parents = ARRAY_APPEND(id, ?)", "age = ARRAY_REMOVE(id, ?)", "info = NULL"], ["last_name NOT IN ('Anderson', 'Johnson', 'Smith')"], ) - check querycompare(q, sql("UPDATE table SET parents = ARRAY_APPEND(id, ?), age = ARRAY_REMOVE(id, ?), info = NULL WHERE last_name NOT IN ('Anderson', 'Johnson', 'Smith')")) \ No newline at end of file + check querycompare(q, sql("UPDATE table SET parents = ARRAY_APPEND(id, ?), age = ARRAY_REMOVE(id, ?), info = NULL WHERE last_name NOT IN ('Anderson', 'Johnson', 'Smith')")) + + diff --git a/tests/update/test_update_arrays.nim b/tests/update/test_update_arrays.nim new file mode 100644 index 0000000..45e60bc --- /dev/null +++ b/tests/update/test_update_arrays.nim @@ -0,0 +1,115 @@ +# Copyright Thomas T. Jarløv (TTJ) - ttj@ttj.dk + +when NimMajor >= 2: + import db_connector/db_common +else: + import std/db_common + +import + std/unittest + +import + src/sqlbuilder, + src/sqlbuilderpkg/utils_private + + + + + +suite "arrays": + + test "[manual] update arrays - ARRAY_REMOVE": + + let q = sqlUpdate( + "table", + ["name", "project_ids = ARRAY_REMOVE(project_ids, ?)"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET name = ?, project_ids = ARRAY_REMOVE(project_ids, ?) WHERE id = ?")) + + + test "[manual] update arrays - only ARRAY_REMOVE": + + let q = sqlUpdate( + "table", + ["project_ids = ARRAY_REMOVE(project_ids, ?)"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_REMOVE(project_ids, ?) WHERE id = ?")) + + + test "[dedicated] update array - ARRAY_REMOVE": + + let q = sqlUpdateArrayRemove( + "table", + ["project_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_REMOVE(project_ids, ?) WHERE id = ?")) + + + test "[dedicated] update arrays - ARRAY_REMOVE": + + let q = sqlUpdateArrayRemove( + "table", + ["project_ids", "task_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_REMOVE(project_ids, ?), task_ids = ARRAY_REMOVE(task_ids, ?) WHERE id = ?")) + + + test "[dedicated] update array - ARRAY_APPEND": + + let q = sqlUpdateArrayAppend( + "table", + ["project_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_APPEND(project_ids, ?) WHERE id = ?")) + + + test "[dedicated] update arrays - ARRAY_APPEND": + + let q = sqlUpdateArrayAppend( + "table", + ["project_ids", "task_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_APPEND(project_ids, ?), task_ids = ARRAY_APPEND(task_ids, ?) WHERE id = ?")) + + + + test "[macro] update array - ARRAY_REMOVE": + + let q = sqlUpdateMacroArrayRemove( + "table", + ["project_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_REMOVE(project_ids, ?) WHERE id = ?")) + + + test "[macro] update array - ARRAY_APPEND": + + let q = sqlUpdateMacroArrayAppend( + "table", + ["project_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_APPEND(project_ids, ?) WHERE id = ?")) + + + test "[macro] update arrays - ARRAY_APPEND": + + let q = sqlUpdateMacroArrayAppend( + "table", + ["project_ids", "task_ids"], + ["id ="], + ) + check querycompare(q, sql("UPDATE table SET project_ids = ARRAY_APPEND(project_ids, ?), task_ids = ARRAY_APPEND(task_ids, ?) WHERE id = ?")) + + + + + +# ORDER BY array_position(array[" & projectIDs & "], project.id) From 6f6afc94737bf6905e885b5dbdcca44063c6368b Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 11:01:14 +0100 Subject: [PATCH 19/27] Update nimble --- sqlbuilder.nimble | 78 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/sqlbuilder.nimble b/sqlbuilder.nimble index 8939282..624a16b 100644 --- a/sqlbuilder.nimble +++ b/sqlbuilder.nimble @@ -1,6 +1,6 @@ # Package -version = "1.0.0" +version = "1.0.1" author = "ThomasTJdev" description = "SQL builder" license = "MIT" @@ -12,3 +12,79 @@ srcDir = "src" requires "nim >= 0.20.2" when NimMajor >= 2: requires "db_connector >= 0.1.0" + + + + + +proc runLegacy() = + exec "nim c -d:dev -r tests/legacy_convert/test_legacy.nim" + exec "nim c -d:dev -r tests/legacy_convert/test_legacy_with_softdelete.nim" + exec "nim c -d:dev -r tests/legacy_convert/test_legacy_with_softdelete2.nim" + +task testlegacy, "Test legacy": + runLegacy() + + +proc runSelect() = + exec "nim c -d:dev -r tests/select/test_select.nim" + exec "nim c -d:dev -r tests/select/test_select_deletemarker.nim" + exec "nim c -d:dev -r tests/select/test_select_const.nim" + exec "nim c -d:dev -r tests/select/test_select_const_deletemarker.nim" + +task testselect, "Test select statement": + runSelect() + + +proc runInsert() = + exec "nim c -d:dev -r tests/insert/test_insert.nim" + +task testinsert, "Test insert statement": + runInsert() + + +proc runUpdate() = + exec "nim c -d:dev -r tests/update/test_update.nim" + exec "nim c -d:dev -r tests/update/test_update_arrays.nim" + +task testupdate, "Test update statement": + runUpdate() + + +proc runDelete() = + exec "nim c -d:dev -r tests/delete/test_delete.nim" + +task testdelete, "Test delete statement": + runDelete() + + +proc runQueryCalls() = + exec "nim c -d:dev -r tests/query_calls/test_query_calls.nim" + +task testquerycalls, "Test query calls": + runQueryCalls() + + +proc runToTypes() = + exec "nim c -d:dev -r tests/totypes/test_result_to_types.nim" + +task testresulttotypes, "Test result to types": + runToTypes() + + +proc runArgs() = + exec "nim c -d:dev -r tests/custom_args/test_args.nim" + +task testargs, "Test args": + runArgs() + + +task test, "Test": + runLegacy() + runSelect() + runInsert() + runUpdate() + runDelete() + runQueryCalls() + runToTypes() + runArgs() \ No newline at end of file From 5b52d1175ce44cbc4f7e6f9db7adf81b5c30205d Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Wed, 20 Dec 2023 11:01:52 +0100 Subject: [PATCH 20/27] Update README --- README.md | 322 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 177 insertions(+), 145 deletions(-) diff --git a/README.md b/README.md index 466ecf9..b44a91b 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ ease the creating of queries. - [Examples (UPDATE)](#examples-update) - [Examples (SELECT)](#examples-select) - Utilities + - [Custom args](#custom-args) - [Dynamic selection of columns](#dynamic-selection-of-columns) - [Query calls for the lazy](#query-calls-for-the-lazy) - [Convert result to types](#convert-result-to-types) @@ -42,11 +43,14 @@ import sqlbuilder import sqlbuilder/select ``` -## Import all but with legacy softdelete fix +## Import and set global soft delete marker ```nim -# nano sqlfile.nim -# import this file instead of sqlbuilder +const tablesWithDeleteMarkerInit = ["table_with_deletemarker"] +include src/sqlbuilder_include +``` +## Import all but with legacy softdelete fix +```nim import src/sqlbuilder/sqlbuilderpkg/insert export insert @@ -69,28 +73,48 @@ include src/sqlbuilderpkg/select # Macro generated queries -The library supports generating some queries with a macro, which improves the +The library supports generating some queries with a macro which can improve the performance due to query being generated on compile time. -The macro generated -queries **do not** accept the `genArgs()` and `genArgsColumns()` due to not -being available on compile time - so there's currently not NULL-support for -macros. - # NULL values -The `genArgs` and `genArgsSetNull` allows you to pass NULL values to the queries. -These are now only needed for insert-queries. +After Nim's update to 0.19.0, the check for NULL values was removed +due to the removal of `nil`. + +You can use `NULL` values in different ways. See the examples. +## Inline in query -## A NULL value +```nim +sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name", "description", "created", "updated", "completed"], + where = @["id =", "name != NULL", "description = NULL"], + useDeleteMarker = false + ) +check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) +``` + +```nim +sqlUpdate( + "table", + ["name", "age", "info = NULL"], + ["id ="], + ) +check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ?")) +``` + +## Custom args + +### A NULL value The global ``const dbNullVal`` represents a NULL value. Use ``dbNullVal`` -in your args, if you need to insert/update to a NULL value. +in your args if you need to insert/update to a NULL value. -## Insert value or NULL +### Insert value or NULL The global ``proc dbValOrNull()`` will check, if it contains a value or is empty. If it contains a value, the value will be used in the args, @@ -98,149 +122,88 @@ otherwise a NULL value (``dbNullVal``) will be used. ``dbValOrNull()`` accepts all types due to `value: auto`. -## Auto NULL-values +### Auto NULL-values There are two generators, which can generate the `NULL` values for you. * `genArgs` does only set a field to `NULL` if `dbNullVal`/`dbValOrNull()` is passed. * `genArgsSetNull` sets empty field (`""` / `c.len() == 0`) to `NULL`. -## Executing DB commands -The examples below support the various DB commands such as ``exec``, -``tryExec``, ``insertID``, ``tryInsertID``, etc. -# Examples (NULL values) -All the examples uses a table named: ``myTable`` and they use the WHERE argument on: ``name``. -## Update string & int -### Version 1 -*Required if NULL values could be expected* -```nim - let a = genArgs("em@em.com", 20, "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> string, int - # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? -``` +# Examples (INSERT) +## Insert default -### Version 2 ```nim - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"]), "em@em.com", 20, "John") - # ==> string, int - # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? +test = sqlInsert("my-table", ["name", "age"]) +check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, ?)")) ``` - -### Version 3 ```nim - let a = genArgsSetNull("em@em.com", "", "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> string, NULL - # ==> UPDATE myTable SET email = ?, age = NULL WHERE name = ? +test = sqlInsert("my-table", ["name", "age = NULL"]) +check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) ``` - -## Update NULL & int - ```nim - let a = genArgs("", 20, "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> NULL, int - # ==> UPDATE myTable SET email = NULL, age = ? WHERE name = ? +let vals = @["thomas", ""] +test = sqlInsert("my-table", ["name", "age"], vals) +check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) ``` - -## Update string & NULL - ```nim - a = genArgs("aa@aa.aa", dbNullVal, "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> string, NULL - # ==> UPDATE myTable SET email = ?, age = NULL WHERE name = ? +test = sqlInsertMacro("my-table", ["name", "age = NULL"]) +check querycompare(test, sql("INSERT INTO my-table (name, age) VALUES (?, NULL)")) ``` - -## Error: Update string & NULL into an integer column - -An empty string, "", will be inserted into the database as NULL. -Empty string cannot be used for an INTEGER column. You therefore -need to use the ``dbValOrNull()`` or ``dbNullVal`` for ``int-values``. - -This is due to, that the library does not know you DB-architecture, so it -is your responsibility to respect the columns. - -```nim - a = genArgs("aa@aa.aa", "", "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> string, ERROR - # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? - # ==> To insert a NULL into a int-field, it is required to use dbValOrNull() - # or dbNullVal, it is only possible to pass and empty string. -``` - - -## Update NULL & NULL - ```nim - let cc = "" - a = genArgs(dbValOrNull(cc), dbValOrNull(cc), "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> NULL, NULL - # ==> UPDATE myTable SET email = NULL, age = NULL WHERE name = ? + let a = genArgs("em@em.com", dbNullVal) + exec(db, sqlInsert("myTable", ["email", "age"], a.query), a.args) + # ==> INSERT INTO myTable (email) VALUES (?) ``` - -## Update unknow value - maybe NULL +# Examples (UPDATE) ```nim - a = genArgs(dbValOrNull(var1), dbValOrNull(var2), "John") - exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) - # ==> AUTO or NULL, AUTO or NULL, string +let q = sqlUpdate( + "table", + ["name", "age", "info"], + ["id ="], + ) +check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = ? WHERE id = ?")) ``` - - - -# Examples (INSERT) - -## Insert default - -```nim - exec(db, sqlInsert("myTable", ["email", "age"]), "em@em.com" , 20) - # OR - insertID(db, sqlInsert("myTable", ["email", "age"]), "em@em.com", 20) - # ==> INSERT INTO myTable (email, age) VALUES (?, ?) -``` ```nim - exec(db, sqlInsertMacro("myTable", ["email", "age"]), "em@em.com" , 20) - # OR - insertID(db, sqlInsertMacro("myTable", ["email", "age"]), "em@em.com", 20) - # ==> INSERT INTO myTable (email, age) VALUES (?, ?) +let q = sqlUpdate( + "table", + ["name", "age", "info = NULL"], + ["id ="], + ) +check querycompare(q, sql("UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ?")) ``` -## Insert with NULL - ```nim - let a = genArgs("em@em.com", dbNullVal) - exec(db, sqlInsert("myTable", ["email", "age"], a.query), a.args) - # OR - insertID(db, sqlInsert("myTable", ["email", "age"], a.query), a.args) - # ==> INSERT INTO myTable (email) VALUES (?) +let q = sqlUpdate( + "table", + ["name = NULL", "age", "info = NULL"], + ["id =", "epoch >", "parent IS NULL", "name IS NOT NULL", "age != 22", "age !="], + ) +check querycompare(q, sql("UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ?")) ``` - -# Examples (UPDATE) - ```nim - # sqlUpdate or sqlUpdateMacro - let q = sqlUpdate("table", ["name", "age", "info = NULL"], ["id ="]) - # ==> UPDATE table SET name = ?, age = ?, info = NULL WHERE id = ? +let q = sqlUpdate( + "table", + ["parents = ARRAY_APPEND(id, ?)", "age = ARRAY_REMOVE(id, ?)", "info = NULL"], + ["last_name NOT IN ('Anderson', 'Johnson', 'Smith')"], + ) +check querycompare(q, sql("UPDATE table SET parents = ARRAY_APPEND(id, ?), age = ARRAY_REMOVE(id, ?), info = NULL WHERE last_name NOT IN ('Anderson', 'Johnson', 'Smith')")) ``` ```nim @@ -253,41 +216,16 @@ is your responsibility to respect the columns. # ==> UPDATE table SET name = NULL, age = ?, info = NULL WHERE id = ? AND epoch > ? AND parent IS NULL AND name IS NOT NULL AND age != 22 AND age != ? ``` +```nim +let a2 = genArgsSetNull("hje", "") +let q = sqlUpdate("my-table", ["name", "age"], ["id"], a2.query) +check querycompare(q, sql("UPDATE my-table SET name = ?, age = NULL WHERE id = ?")) +``` # Examples (SELECT) -Please note that you have to define the equal symbol for the where clause. This -allows you to use `=`, `!=`, `>`, `<`, etc. - -## Select query builder - -The SELECT builder gives access to the following fields: - -* BASE - * table: string, - * select: varargs[string], - * where: varargs[string], -* JOIN - * joinargs: varargs[tuple[table: string, tableAs: string, on: seq[string]]] = [], - * jointype: SQLJoinType = LEFT, -* WHERE IN - * whereInField: string = "", - * whereInValue: seq[string] = @[], - * whereInValueString: seq[string] = @[], - * whereInValueInt: seq[int] = @[], -* Custom SQL, e.g. ORDER BY - * customSQL: string = "", -* Null checks - * checkedArgs: ArgsContainer.query = @[], -* Table alias - * tableAs: string = table, -* Soft delete - * hideIsDeleted: bool = true, - * tablesWithDeleteMarker: varargs[string] = @[], - * deleteMarker = ".is_deleted IS NULL", - ## Example on builder ```nim test = sqlSelect( @@ -397,6 +335,97 @@ check querycompare(test, sql(""" +# Custom args +## Update string & int + +### Version 1 +*Required if NULL values could be expected* +```nim + let a = genArgs("em@em.com", 20, "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> string, int + # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? +``` + + +### Version 2 +```nim + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"]), "em@em.com", 20, "John") + # ==> string, int + # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? +``` + + +### Version 3 +```nim + let a = genArgsSetNull("em@em.com", "", "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> string, NULL + # ==> UPDATE myTable SET email = ?, age = NULL WHERE name = ? +``` + + +## Update NULL & int + +```nim + let a = genArgs("", 20, "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> NULL, int + # ==> UPDATE myTable SET email = NULL, age = ? WHERE name = ? +``` + + +## Update string & NULL + +```nim + a = genArgs("aa@aa.aa", dbNullVal, "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> string, NULL + # ==> UPDATE myTable SET email = ?, age = NULL WHERE name = ? +``` + + +## Error: Update string & NULL into an integer column + +An empty string, "", will be inserted into the database as NULL. +Empty string cannot be used for an INTEGER column. You therefore +need to use the ``dbValOrNull()`` or ``dbNullVal`` for ``int-values``. + +This is due to, that the library does not know you DB-architecture, so it +is your responsibility to respect the columns. + +```nim + a = genArgs("aa@aa.aa", "", "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> string, ERROR + # ==> UPDATE myTable SET email = ?, age = ? WHERE name = ? + # ==> To insert a NULL into a int-field, it is required to use dbValOrNull() + # or dbNullVal, it is only possible to pass and empty string. +``` + + +## Update NULL & NULL + +```nim + let cc = "" + a = genArgs(dbValOrNull(cc), dbValOrNull(cc), "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> NULL, NULL + # ==> UPDATE myTable SET email = NULL, age = NULL WHERE name = ? +``` + + + +## Update unknow value - maybe NULL + +```nim + a = genArgs(dbValOrNull(var1), dbValOrNull(var2), "John") + exec(db, sqlUpdate("myTable", ["email", "age"], ["name"], a.query), a.args) + # ==> AUTO or NULL, AUTO or NULL, string +``` + + + # Dynamic selection of columns Select which columns to include. @@ -456,6 +485,9 @@ This should only be used if: - You have no other way to catch the error - You are to lazy to write the try-except procs yourself +!! These are not available if you use external libraries, e.g. `waterpark`, +!! since they rely on default`DbConn`. + ## Import ```nim From 4957d9538c1e65156ff95521c30ac00ec25652ec Mon Sep 17 00:00:00 2001 From: bung87 Date: Wed, 20 Dec 2023 19:11:28 +0800 Subject: [PATCH 21/27] remove `noJoin` and its usages --- src/sqlbuilderpkg/select.nim | 14 ++++---- tests/select/test_select_const.nim | 32 +++++++++---------- .../select/test_select_const_deletemarker.nim | 4 +-- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/sqlbuilderpkg/select.nim b/src/sqlbuilderpkg/select.nim index 9cfe5f4..b1f9a6c 100644 --- a/src/sqlbuilderpkg/select.nim +++ b/src/sqlbuilderpkg/select.nim @@ -14,8 +14,6 @@ import from ./utils import SQLJoinType, ArgsContainer -const noJoin*: tuple[table: string, tableAs: string, on: seq[string]] = ("", "", @[]) - ## ## Constant generator utilities @@ -33,7 +31,7 @@ proc sqlSelectConstJoin( ): string = var lef = "" - if joinargs == [] or $(joinargs.repr) == "[]": + if joinargs.len == 0: return for d in joinargs: @@ -235,7 +233,7 @@ macro sqlSelectConst*( # Join table var joinTablesUsed: seq[string] - if joinargs[0] != noJoin and joinargs != [] and $(joinargs.repr) != "[]": + if joinargs.len != 0: for i, d in joinargs: if d.repr.len == 0: continue @@ -276,7 +274,7 @@ macro sqlSelectConst*( # Joins # var lef = "" - if joinargs[0] != noJoin: + if joinargs.len != 0: lef = sqlSelectConstJoin(joinargs, jointype) @@ -407,7 +405,7 @@ proc sqlSelect*( # Join table - if joinargs.len() > 0 and joinargs[0] != noJoin: + if joinargs.len() > 0: for d in joinargs: if d.table == "": continue @@ -440,7 +438,7 @@ proc sqlSelect*( # Joins # var lef = "" - if joinargs.len() > 0 and joinargs[0] != noJoin: + if joinargs.len() > 0: for i, d in joinargs: lef.add(" " & $jointype & " JOIN ") lef.add(d.table & " ") @@ -600,7 +598,7 @@ proc sqlSelect*( # Check for missing table alias if ( (tableAs != "" and table != tableAs) or - (joinargs.len() > 0 and joinargs[0] != noJoin) + (joinargs.len() > 0) ): var hit: bool for s in select: diff --git a/tests/select/test_select_const.nim b/tests/select/test_select_const.nim index 396538d..4af15ba 100644 --- a/tests/select/test_select_const.nim +++ b/tests/select/test_select_const.nim @@ -26,7 +26,7 @@ suite "test sqlSelectConst": table = "tasks", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? ")) @@ -41,7 +41,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? ")) @@ -52,7 +52,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = ["t.id ="], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false, customSQL = "ORDER BY t.created DESC" ) @@ -68,7 +68,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND updated > ? AND completed IS ? AND description LIKE ? ")) @@ -80,7 +80,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "updated >", "completed IS", "description LIKE"], - joinargs = [noJoin], + joinargs = [], customSQL = "AND name != 'test' AND created > ? ", useDeleteMarker = false ) @@ -97,7 +97,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "= ANY(ids_array)"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) ")) @@ -113,7 +113,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "= ANY(ids_array)", "= ANY(user_array)", "= ANY(tasks_array)"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? = ANY(ids_array) AND ? = ANY(user_array) AND ? = ANY(tasks_array) ")) @@ -129,7 +129,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "IN (ids_array)"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND ? IN (ids_array) ")) @@ -141,7 +141,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "ids_array"], where = ["id =", "id IN"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, ids_array FROM tasks AS t WHERE id = ? AND id IN (?) ")) @@ -157,7 +157,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name != NULL", "description = NULL"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != NULL AND description = NULL ")) @@ -169,7 +169,7 @@ suite "test sqlSelectConst": tableAs = "t", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id =", "name !=", "description = NULL"], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = false ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks AS t WHERE id = ? AND name != ? AND description = NULL ")) @@ -286,7 +286,7 @@ suite "test sqlSelectConst - joins": tableAs = "t", select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = ["t.id ="], - # joinargs = [noJoin], + # joinargs = [], joinargs = [ (table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]), (table: "projects", tableAs: "", on: @["projects.id = t.project_id", "projects.status = 1"]) @@ -378,7 +378,7 @@ suite "test sqlSelectConst - deletemarkers / softdelete": table = "tasks", select = ["id", "name"], where = ["id ="], - joinargs = [noJoin], + joinargs = [], # joinargs = [(table: "projects", tableAs: "", on: @["projects.id = tasks.project_id", "projects.status = 1"])], # tablesWithDeleteMarker = [] #tableWithDeleteMarkerLet, tablesWithDeleteMarker = ["tasks", "history", "tasksitems"] @@ -541,7 +541,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name", "t.description", "t.created", "t.updated", "t.completed"], where = ["t.id ="], - joinargs = [noJoin], + joinargs = [], tablesWithDeleteMarker = ["tasks", "history", "tasksitems"], #tableWithDeleteMarker ) @@ -651,7 +651,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name"], where = ["t.id ="], - joinargs = [noJoin], + joinargs = [], whereInField = "t.name", whereInValue = ["'1aa'", "'2bb'", "'3cc'"], tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker @@ -673,7 +673,7 @@ suite "sqlSelectConst": tableAs = "t", select = ["t.id", "t.name"], where = ["t.id ="], - joinargs = [noJoin], + joinargs = [], whereInField = "t.id", whereInValue = [""], tablesWithDeleteMarker = ["tasksQ", "history", "tasksitems"], #tableWithDeleteMarker diff --git a/tests/select/test_select_const_deletemarker.nim b/tests/select/test_select_const_deletemarker.nim index 66f85e0..f626890 100644 --- a/tests/select/test_select_const_deletemarker.nim +++ b/tests/select/test_select_const_deletemarker.nim @@ -30,7 +30,7 @@ suite "select with tablesWithDeleteMarkerInit init": table = "tasks", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = true ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) @@ -43,7 +43,7 @@ suite "select with tablesWithDeleteMarkerInit init": table = "tasks", select = ["id", "name", "description", "created", "updated", "completed"], where = ["id ="], - joinargs = [noJoin], + joinargs = [], useDeleteMarker = true ) check querycompare(test, sql("SELECT id, name, description, created, updated, completed FROM tasks WHERE id = ? AND tasks.is_deleted IS NULL ")) From 90a3324907104f57c38cc080fa42bca2f00385d4 Mon Sep 17 00:00:00 2001 From: bung87 Date: Thu, 21 Dec 2023 10:31:06 +0800 Subject: [PATCH 22/27] add cross join test cases --- src/sqlbuilderpkg/utils.nim | 1 + tests/select/test_select.nim | 12 ++++++++++++ tests/select/test_select_const.nim | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/sqlbuilderpkg/utils.nim b/src/sqlbuilderpkg/utils.nim index af6c883..da63aea 100644 --- a/src/sqlbuilderpkg/utils.nim +++ b/src/sqlbuilderpkg/utils.nim @@ -19,6 +19,7 @@ type INNER LEFT RIGHT + CROSS FULL SQLQueryType* = enum diff --git a/tests/select/test_select.nim b/tests/select/test_select.nim index 858eea2..df2d274 100644 --- a/tests/select/test_select.nim +++ b/tests/select/test_select.nim @@ -191,6 +191,18 @@ suite "test sqlSelect - joins": ) check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + test "CROSS JOIN": + var test: SqlQuery + + test = sqlSelect( + table = "a", + select = @["id"], + joinargs = @[(table: "b", tableAs: "", on: @["a.id = b.id"])], + jointype = CROSS, + useDeleteMarker = false + ) + check querycompare(test, sql("SELECT id FROM a CROSS JOIN b ON (a.id = b.id)")) + suite "test sqlSelect - deletemarkers / softdelete": diff --git a/tests/select/test_select_const.nim b/tests/select/test_select_const.nim index 396538d..4b5bec4 100644 --- a/tests/select/test_select_const.nim +++ b/tests/select/test_select_const.nim @@ -247,6 +247,17 @@ suite "test sqlSelectConst - joins": ) check querycompare(test, sql("SELECT id, name FROM tasks AS t INNER JOIN projects ON (projects.id = t.project_id AND projects.status = 1) WHERE id = ? ")) + test "CROSS JOIN": + var test: SqlQuery + + test = sqlSelectConst( + table = "a", + select = ["id"], + where = [], + joinargs = [(table: "b", tableAs: "", on: @["a.id = b.id"])], + jointype = CROSS, + ) + check querycompare(test, sql("SELECT id FROM a CROSS JOIN b ON (a.id = b.id)")) test "JOIN #1": var test: SqlQuery From 804fb8571a8fe83a8643ef3e7939a95d32a92244 Mon Sep 17 00:00:00 2001 From: bung87 Date: Thu, 21 Dec 2023 11:02:21 +0800 Subject: [PATCH 23/27] rename to , refactor 's clause --- src/sqlbuilderpkg/delete.nim | 37 ++++++++++------------ src/sqlbuilderpkg/update.nim | 48 ++++------------------------- src/sqlbuilderpkg/utils_private.nim | 32 +++++++++++++++++++ tests/delete/test_delete.nim | 3 ++ 4 files changed, 57 insertions(+), 63 deletions(-) diff --git a/src/sqlbuilderpkg/delete.nim b/src/sqlbuilderpkg/delete.nim index 4ca7fb8..c6d55e0 100644 --- a/src/sqlbuilderpkg/delete.nim +++ b/src/sqlbuilderpkg/delete.nim @@ -19,12 +19,9 @@ proc sqlDelete*(table: string, where: varargs[string]): SqlQuery = ## Does NOT check for NULL values var res = "DELETE FROM " & table - var wes = " WHERE " - for i, d in where: - if i > 0: - wes.add(" AND ") - wes.add(formatWhereParams(d)) - result = sql(res & wes) + if where.len > 0: + res.add sqlWhere(where) + result = sql(res) proc sqlDelete*(table: string, where: varargs[string], args: ArgsContainer.query): SqlQuery = @@ -32,15 +29,16 @@ proc sqlDelete*(table: string, where: varargs[string], args: ArgsContainer.query ## Checks for NULL values var res = "DELETE FROM " & table - var wes = " WHERE " - for i, d in where: - if i > 0: - wes.add(" AND ") - if args[i].isNull: - wes.add(d & " = NULL") - else: - wes.add(d & " = ?") - result = sql(res & wes) + if where.len > 0: + res.add " WHERE " + for i, d in where: + if i > 0: + res.add(" AND ") + if args[i].isNull: + res.add(d & " = NULL") + else: + res.add(d & " = ?") + result = sql(res) macro sqlDeleteMacro*(table: string, where: varargs[string]): SqlQuery = @@ -48,9 +46,6 @@ macro sqlDeleteMacro*(table: string, where: varargs[string]): SqlQuery = ## Does NOT check for NULL values var res = "DELETE FROM " & $table - var wes = " WHERE " - for i, d in where: - if i > 0: - wes.add(" AND ") - wes.add(formatWhereParams($d)) - result = parseStmt("sql(\"" & res & wes & "\")") + if where.len > 0: + res.add sqlWhere(where) + result = parseStmt("sql(\"" & res & "\")") diff --git a/src/sqlbuilderpkg/update.nim b/src/sqlbuilderpkg/update.nim index 82bcaa9..977bfa3 100644 --- a/src/sqlbuilderpkg/update.nim +++ b/src/sqlbuilderpkg/update.nim @@ -72,22 +72,6 @@ proc updateArray(arrayType: string, arrayAppend: varargs[string]): string = return result -proc updateWhere(where: varargs[string]): string = - ## Update the WHERE part of the query. - ## - ## => ["name", "age = "] - ## => `WHERE name = ?, age = ?` - ## - ## => ["name = ", "age >"] - ## => `WHERE name = ?, age > ?` - var wes = " WHERE " - for i, v in where: - if i > 0: - wes.add(" AND ") - wes.add(formatWhereParams(v)) - return wes - - proc sqlUpdate*(table: string, data: varargs[string], where: varargs[string], args: ArgsContainer.query): SqlQuery = ## SQL builder for UPDATE queries ## Checks for NULL values @@ -130,7 +114,7 @@ proc sqlUpdate*( ## where => ["id = ", "name IS NULL"] var fields: string fields.add(updateSet(data)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = sql("UPDATE " & table & " SET " & fields) @@ -142,7 +126,7 @@ proc sqlUpdateArrayRemove*( ## ARRAY_REMOVE var fields: string fields.add(updateArray("ARRAY_REMOVE", arrayRemove)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = sql("UPDATE " & table & " SET " & fields) @@ -154,7 +138,7 @@ proc sqlUpdateArrayAppend*( ## ARRAY_APPEND var fields: string fields.add(updateArray("ARRAY_APPEND", arrayAppend)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = sql("UPDATE " & table & " SET " & fields) @@ -179,26 +163,6 @@ proc updateSet(data: NimNode): string = return result - -proc updateWhere(where: NimNode): string = - ## Update the WHERE part of the query. - ## - ## => ["name", "age = "] - ## => `WHERE name = ?, age = ?` - ## - ## => ["name = ", "age >"] - ## => `WHERE name = ?, age > ?` - var wes = " WHERE " - for i, v in where: - # Convert NimNode to string - let d = $v - if i > 0: - wes.add(" AND ") - wes.add(formatWhereParams(d)) - return wes - - - macro sqlUpdateMacro*( table: string, data: varargs[string], @@ -212,7 +176,7 @@ macro sqlUpdateMacro*( ## where => ["id = ", "name IS NULL"] var fields: string fields.add(updateSet(data)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") @@ -243,7 +207,7 @@ macro sqlUpdateMacroArrayRemove*( ## ARRAY_REMOVE macro var fields: string fields.add(updateArray("ARRAY_REMOVE", arrayRemove)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") @@ -255,5 +219,5 @@ macro sqlUpdateMacroArrayAppend*( ## ARRAY_APPEND macro var fields: string fields.add(updateArray("ARRAY_APPEND", arrayAppend)) - fields.add(updateWhere(where)) + fields.add(sqlWhere(where)) result = parseStmt("sql(\"" & "UPDATE " & $table & " SET " & fields & "\")") diff --git a/src/sqlbuilderpkg/utils_private.nim b/src/sqlbuilderpkg/utils_private.nim index a6d610e..a752dd3 100644 --- a/src/sqlbuilderpkg/utils_private.nim +++ b/src/sqlbuilderpkg/utils_private.nim @@ -7,6 +7,7 @@ else: import + std/macros, std/strutils @@ -139,3 +140,34 @@ proc hasIllegalFormats*(query: string): string = if "??" in noSpaces: return "double insert detected. (??)" +proc sqlWhere*(where: varargs[string]): string = + ## the WHERE part of the query. + ## + ## => ["name", "age = "] + ## => `WHERE name = ?, age = ?` + ## + ## => ["name = ", "age >"] + ## => `WHERE name = ?, age > ?` + var wes = " WHERE " + for i, v in where: + if i > 0: + wes.add(" AND ") + wes.add(formatWhereParams(v)) + return wes + +proc sqlWhere*(where: NimNode): string = + ## the WHERE part of the query. + ## + ## => ["name", "age = "] + ## => `WHERE name = ?, age = ?` + ## + ## => ["name = ", "age >"] + ## => `WHERE name = ?, age > ?` + var wes = " WHERE " + for i, v in where: + # Convert NimNode to string + let d = $v + if i > 0: + wes.add(" AND ") + wes.add(formatWhereParams(d)) + return wes \ No newline at end of file diff --git a/tests/delete/test_delete.nim b/tests/delete/test_delete.nim index 36bffd9..df767aa 100644 --- a/tests/delete/test_delete.nim +++ b/tests/delete/test_delete.nim @@ -24,6 +24,9 @@ suite "delete - normal": test = sqlDelete("my-table", ["name", "age"]) check querycompare(test, sql("DELETE FROM my-table WHERE name = ? AND age = ?")) + test = sqlDelete("my-table", []) + check querycompare(test, sql("DELETE FROM my-table")) + test "sqlDeleteWhere": var test: SqlQuery From fcadd9435d7dab663ba0ad43f2c182f52d6a9774 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Thu, 21 Dec 2023 07:38:05 +0100 Subject: [PATCH 24/27] Github actions --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2f2cc4d..4ef6055 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,9 +2,11 @@ name: Build sqlbuilder on: push: - branches: [ master ] + branches: + - '*' pull_request: - branches: [ master ] + branches: + - '*' jobs: build: From f3d9d70f3fbfb54a4baa90684d679a5d6f1cee4f Mon Sep 17 00:00:00 2001 From: bung87 Date: Thu, 21 Dec 2023 14:39:58 +0800 Subject: [PATCH 25/27] delete test cases with genargs --- src/sqlbuilderpkg/delete.nim | 9 +-------- tests/delete/test_delete.nim | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/sqlbuilderpkg/delete.nim b/src/sqlbuilderpkg/delete.nim index c6d55e0..afc5748 100644 --- a/src/sqlbuilderpkg/delete.nim +++ b/src/sqlbuilderpkg/delete.nim @@ -30,14 +30,7 @@ proc sqlDelete*(table: string, where: varargs[string], args: ArgsContainer.query var res = "DELETE FROM " & table if where.len > 0: - res.add " WHERE " - for i, d in where: - if i > 0: - res.add(" AND ") - if args[i].isNull: - res.add(d & " = NULL") - else: - res.add(d & " = ?") + res.add(sqlWhere(where)) result = sql(res) diff --git a/tests/delete/test_delete.nim b/tests/delete/test_delete.nim index df767aa..42385e2 100644 --- a/tests/delete/test_delete.nim +++ b/tests/delete/test_delete.nim @@ -67,4 +67,32 @@ suite "delete - macro": check querycompare(test, sql("DELETE FROM my-table WHERE name IS NULL AND age = ?")) +suite "delete - genArgs": + + test "sqlDelete with genArgs": + + var a = genArgs("123", dbNullVal) + + var test = sqlDelete("tasksQQ", ["id =", "status IS"], a.query) + + check querycompare(test, sql("""DELETE FROM tasksQQ WHERE id = ? AND status IS ?""")) + + + + a = genArgs("123", dbNullVal, dbNullVal) + + test = sqlDelete("tasksQQ", ["id =", "status IS NOT", "phase IS"], a.query) + + check querycompare(test, sql("""DELETE FROM tasksQQ WHERE id = ? AND status IS NOT ? AND phase IS ?""")) + + + + test "sqlDelete with genArgsSetNull": + + var a = genArgsSetNull("123", "", "") + + var test = sqlDelete("tasksQQ", ["id =", "status IS NOT", "phase IS"], a.query) + + check querycompare(test, sql("""DELETE FROM tasksQQ WHERE id = ? AND status IS NOT ? AND phase IS ?""")) + From a9073f22642404e3e8f19a7ba9c9cbed88bff2e3 Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Thu, 21 Dec 2023 07:43:37 +0100 Subject: [PATCH 26/27] Github actions --- .github/workflows/main.yml | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ef6055..5bc3cc3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,26 +10,17 @@ on: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: - os: - - ubuntu-latest - version: - - 1.6.14 - - stable - + nim: + - '1.6.14' + - 'stable' + name: Nim ${{ matrix.nim }} sample steps: - - uses: actions/checkout@v1 - - uses: jiro4989/setup-nim-action@master - with: - nim-version: ${{ matrix.version }} - - - name: Print Nim version - run: nim -v - - - name: Print Nimble version - run: nimble -v - - - name: Build binaries - run: nimble test \ No newline at end of file + - uses: actions/checkout@v3 + - name: Setup nim + uses: jiro4989/setup-nim-action@v1 + with: + nim-version: ${{ matrix.nim }} + - run: nimble test -Y \ No newline at end of file From 1ffa820e6f7b4ef101263b9e0f6595a0fc4f2f0d Mon Sep 17 00:00:00 2001 From: ThomasTJdev Date: Thu, 21 Dec 2023 08:44:05 +0100 Subject: [PATCH 27/27] A couple more test cases --- tests/select/test_select.nim | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/select/test_select.nim b/tests/select/test_select.nim index df2d274..bc0cc88 100644 --- a/tests/select/test_select.nim +++ b/tests/select/test_select.nim @@ -150,6 +150,45 @@ suite "test sqlSelect": suite "test sqlSelect - joins": + test "LEFT JOIN [no values] using empty []": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = @["id", "name"], + where = @["id ="], + joinargs = @[], + useDeleteMarker = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t WHERE id = ? ")) + + test "LEFT JOIN [no values] using varargs instead of seq": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [], + useDeleteMarker = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t WHERE id = ? ")) + + test "LEFT JOIN using AS values with varargs": + var test: SqlQuery + + test = sqlSelect( + table = "tasks", + tableAs = "t", + select = ["id", "name"], + where = ["id ="], + joinargs = [(table: "projects", tableAs: "p", on: @["p.id = t.project_id", "p.status = 1"])], + useDeleteMarker = false + ) + check querycompare(test, sql("SELECT id, name FROM tasks AS t LEFT JOIN projects AS p ON (p.id = t.project_id AND p.status = 1) WHERE id = ? ")) + test "LEFT JOIN using AS values": var test: SqlQuery