diff --git a/.gitignore b/.gitignore index bf6670e..2ac0a92 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .DS_Store .idea +.update.timestamp .vscode /lib /nimcache diff --git a/sqlcipher.nimble b/sqlcipher.nimble index 579b30f..ef7778c 100644 --- a/sqlcipher.nimble +++ b/sqlcipher.nimble @@ -45,3 +45,4 @@ proc buildAndRunTest(name: string, task tests, "Run all tests": buildAndRunTest "db_smoke" + buildAndRunTest "sqlite_boolean_literals" diff --git a/sqlcipher/tiny_sqlite.nim b/sqlcipher/tiny_sqlite.nim index 795b16e..b29ca32 100644 --- a/sqlcipher/tiny_sqlite.nim +++ b/sqlcipher/tiny_sqlite.nim @@ -1,4 +1,4 @@ -import std / [options, macros, typetraits], sequtils, unicode +import std / [options, macros, typetraits], sequtils, unicode, strutils from sqlite_wrapper as sqlite import nil from stew/shims/macros as stew_macros import hasCustomPragmaFixed, getCustomPragmaFixed @@ -179,7 +179,24 @@ proc toDbValues*(values: varargs[DbValue, toDbValue]): seq[DbValue] = proc fromDbValue*(value: DbValue, T: typedesc[Ordinal]): T = # Convert a DbValue to an ordinal. - value.intVal.T + + ### START CUSTOM SUPPORT ### + # FOR STRING REPRESENTATIONS OF BOOLEANS. + # SQLITE VERSIONS <3.23.0 DID NOT AUTOMATICALLY TRANSLATE BOOLEAN + # LITERALS IN SQL SYNTAX TO 0/1. SEE https://sqlite.org/lang_expr.html + # (section 14) FOR MORE DETAIL. + when T is bool: + if (value.kind == sqliteText): + try: + result = value.strVal.toLower.parseBool + except ValueError as ve: + ve.msg = "Error parsing string value into boolean: " & ve.msg + raise ve + else: + result = value.intVal.T + else: + ### END CUSTOM SUPPORT ### + value.intVal.T proc fromDbValue*(value: DbValue, T: typedesc[SomeFloat]): float64 = ## Convert a DbValue to a float. diff --git a/test/sqlite_boolean_literals.nim b/test/sqlite_boolean_literals.nim new file mode 100644 index 0000000..ef79953 --- /dev/null +++ b/test/sqlite_boolean_literals.nim @@ -0,0 +1,172 @@ +import # nim libs + options, os, unittest, strformat + +import # vendor libs + ../sqlcipher + +type + BoolTest* {.dbTableName("boolTest").} = object + testId* {.dbColumnName("testId").}: string + boolCol1* {.dbColumnName("boolCol1").}: bool + boolCol2* {.dbColumnName("boolCol2").}: bool + boolCol3* {.dbColumnName("boolCol3").}: Option[bool] + SqliteBool = bool | int | string | Option[bool] + +proc createBoolTable(db: DbConn) = + var boolTest: BoolTest + db.exec(fmt"""CREATE TABLE {boolTest.tableName} ( + {boolTest.testId.columnName} VARCHAR NOT NULL PRIMARY KEY, + {boolTest.boolCol1.columnName} BOOLEAN, + {boolTest.boolCol2.columnName} BOOLEAN DEFAULT TRUE, + {boolTest.boolCol3.columnName} BOOLEAN + )""") + +proc getBoolRow(db: DbConn, testId: string): BoolTest = + var boolTest: BoolTest + let query = fmt"""SELECT * FROM {boolTest.tableName} WHERE {boolTest.testId.columnName} = ?""" + let resultOption = db.one(BoolTest, query, testId) + if resultOption.isNone: + raise newException(ValueError, fmt"Failed to get row with testId '{testId}'") + resultOption.get() + +proc insertBoolRowString(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + let bool3Param = if bool3.isNone: "null" else: fmt"'{bool3.get}'" + db.execScript(fmt"""INSERT INTO {boolTest.tableName} VALUES ('{testId}', '{bool1}', '{bool2}', {bool3Param});""") + db.getBoolRow(testId) + +proc insertBoolRowString(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + let bool3Param = if bool3.isNone: "null" else: fmt"'{bool3.get}'" + db.execScript(fmt""" + INSERT INTO {boolTest.tableName} ( + {boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName} + ) + VALUES ( + '{testId}', '{bool1}', {bool3Param} + );""") + db.getBoolRow(testId) + +proc insertBoolRowInt(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + let bool3Param = if bool3.isNone: "null" else: fmt"{bool3.get}" + db.execScript(fmt"""INSERT INTO {boolTest.tableName} VALUES ('{testId}', {bool1}, {bool2}, {bool3Param});""") + db.getBoolRow(testId) + +proc insertBoolRowInt(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + let bool3Param = if bool3.isNone: "null" else: fmt"{bool3.get}" + db.execScript(fmt"""INSERT INTO {boolTest.tableName} ({boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName}) VALUES ('{testId}', {bool1}, {bool3Param});""") + db.getBoolRow(testId) + +proc insertBoolRowBool(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + db.exec(fmt"""INSERT INTO {boolTest.tableName} VALUES (?, ?, ?, ?);""", testId, bool1, bool2, bool3) + db.getBoolRow(testId) + +proc insertBoolRowBool(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest = + var boolTest: BoolTest + db.exec(fmt"""INSERT INTO {boolTest.tableName} ({boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName}) VALUES (?, ?, ?);""", testId, bool1, bool3) + db.getBoolRow(testId) + +suite "sqlite_booleans": + let password = "qwerty" + let path = currentSourcePath.parentDir() & "/build/my.db" + removeFile(path) + let db = openDatabase(path) + db.key(password) + debugEcho "creating bool test table" + db.createBoolTable() + + test "using nim boolean types": + let falseFalseNone1 = db.insertBoolRowBool("falseFalseNone1", false, false, bool.none) + let falseTrueFalse1 = db.insertBoolRowBool("falseTrueFalse1", false, false.some) + let trueFalseNone1 = db.insertBoolRowBool("trueFalseNone1", true, false, bool.none) + let trueTrueTrue1 = db.insertBoolRowBool("trueTrueTrue1", true, true.some) + + check: + falseFalseNone1 == BoolTest(testId: "falseFalseNone1", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse1 == BoolTest(testId: "falseTrueFalse1", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone1 == BoolTest(testId: "trueFalseNone1", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue1 == BoolTest(testId: "trueTrueTrue1", boolCol1: true, boolCol2: true, boolCol3: true.some) + + test "using strings": + let falseFalseNone2 = db.insertBoolRowString("falseFalseNone2", "false", "false", string.none) + let falseTrueFalse2 = db.insertBoolRowString("falseTrueFalse2", "false", "false".some) + let trueFalseNone2 = db.insertBoolRowString("trueFalseNone2", "true", "false", string.none) + let trueTrueTrue2 = db.insertBoolRowString("trueTrueTrue2", "true", "true".some) + + check: + falseFalseNone2 == BoolTest(testId: "falseFalseNone2", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse2 == BoolTest(testId: "falseTrueFalse2", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone2 == BoolTest(testId: "trueFalseNone2", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue2 == BoolTest(testId: "trueTrueTrue2", boolCol1: true, boolCol2: true, boolCol3: true.some) + + + test "using uppercase strings": + let falseFalseNone3 = db.insertBoolRowString("falseFalseNone3", "FALSE", "FALSE", string.none) + let falseTrueFalse3 = db.insertBoolRowString("falseTrueFalse3", "FALSE", "FALSE".some) + let trueFalseNone3 = db.insertBoolRowString("trueFalseNone3", "TRUE", "FALSE", string.none) + let trueTrueTrue3 = db.insertBoolRowString("trueTrueTrue3", "TRUE", "TRUE".some) + + check: + falseFalseNone3 == BoolTest(testId: "falseFalseNone3", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse3 == BoolTest(testId: "falseTrueFalse3", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone3 == BoolTest(testId: "trueFalseNone3", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue3 == BoolTest(testId: "trueTrueTrue3", boolCol1: true, boolCol2: true, boolCol3: true.some) + + + test "using ints": + let falseFalseNone4 = db.insertBoolRowInt("falseFalseNone4", 0, 0, int.none) + let falseTrueFalse4 = db.insertBoolRowInt("falseTrueFalse4", 0, 0.some) + let trueFalseNone4 = db.insertBoolRowInt("trueFalseNone4", 1, 0, int.none) + let trueTrueTrue4 = db.insertBoolRowInt("trueTrueTrue4", 1, 1.some) + + check: + falseFalseNone4 == BoolTest(testId: "falseFalseNone4", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse4 == BoolTest(testId: "falseTrueFalse4", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone4 == BoolTest(testId: "trueFalseNone4", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue4 == BoolTest(testId: "trueTrueTrue4", boolCol1: true, boolCol2: true, boolCol3: true.some) + + test "using string ints": + let falseFalseNone5 = db.insertBoolRowString("falseFalseNone5", "0", "0", string.none) + let falseTrueFalse5 = db.insertBoolRowString("falseTrueFalse5", "0", "0".some) + let trueFalseNone5 = db.insertBoolRowString("trueFalseNone5", "1", "0", string.none) + let trueTrueTrue5 = db.insertBoolRowString("trueTrueTrue5", "1", "1".some) + + check: + falseFalseNone5 == BoolTest(testId: "falseFalseNone5", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse5 == BoolTest(testId: "falseTrueFalse5", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone5 == BoolTest(testId: "trueFalseNone5", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue5 == BoolTest(testId: "trueTrueTrue5", boolCol1: true, boolCol2: true, boolCol3: true.some) + + # Nim's standard lib strutils.parseBool supports yes/no and on/off, see + # https://nim-lang.org/docs/strutils.html#parseBool%2Cstring for more information. + # The following tests are just to illustrate support for these values, as well. + + test "using yes/no": + let falseFalseNone6 = db.insertBoolRowString("falseFalseNone6", "no", "no", string.none) + let falseTrueFalse6 = db.insertBoolRowString("falseTrueFalse6", "no", "no".some) + let trueFalseNone6 = db.insertBoolRowString("trueFalseNone6", "yes", "no", string.none) + let trueTrueTrue6 = db.insertBoolRowString("trueTrueTrue6", "yes", "yes".some) + + check: + falseFalseNone6 == BoolTest(testId: "falseFalseNone6", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse6 == BoolTest(testId: "falseTrueFalse6", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone6 == BoolTest(testId: "trueFalseNone6", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue6 == BoolTest(testId: "trueTrueTrue6", boolCol1: true, boolCol2: true, boolCol3: true.some) + + test "using on/off": + let falseFalseNone7 = db.insertBoolRowString("falseFalseNone7", "off", "off", string.none) + let falseTrueFalse7 = db.insertBoolRowString("falseTrueFalse7", "off", "off".some) + let trueFalseNone7 = db.insertBoolRowString("trueFalseNone7", "on", "off", string.none) + let trueTrueTrue7 = db.insertBoolRowString("trueTrueTrue7", "on", "on".some) + + check: + falseFalseNone7 == BoolTest(testId: "falseFalseNone7", boolCol1: false, boolCol2: false, boolCol3: bool.none) + falseTrueFalse7 == BoolTest(testId: "falseTrueFalse7", boolCol1: false, boolCol2: true, boolCol3: false.some) + trueFalseNone7 == BoolTest(testId: "trueFalseNone7", boolCol1: true, boolCol2: false, boolCol3: bool.none) + trueTrueTrue7 == BoolTest(testId: "trueTrueTrue7", boolCol1: true, boolCol2: true, boolCol3: true.some) + + db.close() + removeFile(path) diff --git a/vendor/nim-stew b/vendor/nim-stew index ff524ed..3c91b86 160000 --- a/vendor/nim-stew +++ b/vendor/nim-stew @@ -1 +1 @@ -Subproject commit ff524ed832b9933760a5c500252323ec840951a6 +Subproject commit 3c91b8694e15137a81ec7db37c6c58194ec94a6a diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system index 92e5042..a1da1f4 160000 --- a/vendor/nimbus-build-system +++ b/vendor/nimbus-build-system @@ -1 +1 @@ -Subproject commit 92e5042667b747d22106f085eaa9b5e9766ba474 +Subproject commit a1da1f403d9034f0f09582c832863c902751e9fb diff --git a/vendor/sqlcipher b/vendor/sqlcipher index 50376d0..0663d85 160000 --- a/vendor/sqlcipher +++ b/vendor/sqlcipher @@ -1 +1 @@ -Subproject commit 50376d07a5919f1777ac983921facf0bf0fc1976 +Subproject commit 0663d8500204e14bd2bb0ca25162d91e4555528d