Skip to content

Commit d4fa3f0

Browse files
Eric Mastromichaelsbradleyjr
authored andcommitted
feat: support boolean values encoded as text
status-go, owing to its use of a version of sqlcipher that uses a version of sqlite previous to 3.23.0, sometimes inserts (“0”, 0, false, FALSE, “false”, “FALSE”) or (“1”, 1, true, TRUE, “true”, “TRUE”) for boolean values in the database. Here, we are able to properly decode text and integer encodings of booleans in the database into Nim's `bool` type. feat: add unit tests for sqlite boolean literals support
1 parent c165282 commit d4fa3f0

File tree

3 files changed

+192
-2
lines changed

3 files changed

+192
-2
lines changed

sqlcipher.nimble

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,4 @@ proc buildAndRunTest(name: string,
4545

4646
task tests, "Run all tests":
4747
buildAndRunTest "db_smoke"
48+
buildAndRunTest "sqlite_boolean_literals"

sqlcipher/tiny_sqlite.nim

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import std / [options, macros, typetraits], sequtils, unicode
1+
import std / [options, macros, typetraits], sequtils, unicode, strutils
22

33
from sqlite_wrapper as sqlite import nil
44
from stew/shims/macros as stew_macros import hasCustomPragmaFixed, getCustomPragmaFixed
@@ -179,7 +179,24 @@ proc toDbValues*(values: varargs[DbValue, toDbValue]): seq[DbValue] =
179179

180180
proc fromDbValue*(value: DbValue, T: typedesc[Ordinal]): T =
181181
# Convert a DbValue to an ordinal.
182-
value.intVal.T
182+
183+
### START CUSTOM SUPPORT ###
184+
# FOR STRING REPRESENTATIONS OF BOOLEANS.
185+
# SQLITE VERSIONS <3.23.0 DID NOT AUTOMATICALLY TRANSLATE BOOLEAN
186+
# LITERALS IN SQL SYNTAX TO 0/1. SEE https://sqlite.org/lang_expr.html
187+
# (section 14) FOR MORE DETAIL.
188+
when T is bool:
189+
if (value.kind == sqliteText):
190+
try:
191+
result = value.strVal.toLower.parseBool
192+
except ValueError as ve:
193+
ve.msg = "Error parsing string value into boolean: " & ve.msg
194+
raise ve
195+
else:
196+
result = value.intVal.T
197+
else:
198+
### END CUSTOM SUPPORT ###
199+
value.intVal.T
183200

184201
proc fromDbValue*(value: DbValue, T: typedesc[SomeFloat]): float64 =
185202
## Convert a DbValue to a float.

test/sqlite_boolean_literals.nim

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import # nim libs
2+
options, os, unittest, strformat
3+
4+
import # vendor libs
5+
../sqlcipher
6+
7+
type
8+
BoolTest* {.dbTableName("boolTest").} = object
9+
testId* {.dbColumnName("testId").}: string
10+
boolCol1* {.dbColumnName("boolCol1").}: bool
11+
boolCol2* {.dbColumnName("boolCol2").}: bool
12+
boolCol3* {.dbColumnName("boolCol3").}: Option[bool]
13+
SqliteBool = bool | int | string | Option[bool]
14+
15+
proc createBoolTable(db: DbConn) =
16+
var boolTest: BoolTest
17+
db.exec(fmt"""CREATE TABLE {boolTest.tableName} (
18+
{boolTest.testId.columnName} VARCHAR NOT NULL PRIMARY KEY,
19+
{boolTest.boolCol1.columnName} BOOLEAN,
20+
{boolTest.boolCol2.columnName} BOOLEAN DEFAULT TRUE,
21+
{boolTest.boolCol3.columnName} BOOLEAN
22+
)""")
23+
24+
proc getBoolRow(db: DbConn, testId: string): BoolTest =
25+
var boolTest: BoolTest
26+
let query = fmt"""SELECT * FROM {boolTest.tableName} WHERE {boolTest.testId.columnName} = ?"""
27+
let resultOption = db.one(BoolTest, query, testId)
28+
if resultOption.isNone:
29+
raise newException(ValueError, fmt"Failed to get row with testId '{testId}'")
30+
resultOption.get()
31+
32+
proc insertBoolRowString(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
33+
var boolTest: BoolTest
34+
let bool3Param = if bool3.isNone: "null" else: fmt"'{bool3.get}'"
35+
db.execScript(fmt"""INSERT INTO {boolTest.tableName} VALUES ('{testId}', '{bool1}', '{bool2}', {bool3Param});""")
36+
db.getBoolRow(testId)
37+
38+
proc insertBoolRowString(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
39+
var boolTest: BoolTest
40+
let bool3Param = if bool3.isNone: "null" else: fmt"'{bool3.get}'"
41+
db.execScript(fmt"""
42+
INSERT INTO {boolTest.tableName} (
43+
{boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName}
44+
)
45+
VALUES (
46+
'{testId}', '{bool1}', {bool3Param}
47+
);""")
48+
db.getBoolRow(testId)
49+
50+
proc insertBoolRowInt(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
51+
var boolTest: BoolTest
52+
let bool3Param = if bool3.isNone: "null" else: fmt"{bool3.get}"
53+
db.execScript(fmt"""INSERT INTO {boolTest.tableName} VALUES ('{testId}', {bool1}, {bool2}, {bool3Param});""")
54+
db.getBoolRow(testId)
55+
56+
proc insertBoolRowInt(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
57+
var boolTest: BoolTest
58+
let bool3Param = if bool3.isNone: "null" else: fmt"{bool3.get}"
59+
db.execScript(fmt"""INSERT INTO {boolTest.tableName} ({boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName}) VALUES ('{testId}', {bool1}, {bool3Param});""")
60+
db.getBoolRow(testId)
61+
62+
proc insertBoolRowBool(db: DbConn, testId: string, bool1: SqliteBool, bool2: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
63+
var boolTest: BoolTest
64+
db.exec(fmt"""INSERT INTO {boolTest.tableName} VALUES (?, ?, ?, ?);""", testId, bool1, bool2, bool3)
65+
db.getBoolRow(testId)
66+
67+
proc insertBoolRowBool(db: DbConn, testId: string, bool1: SqliteBool, bool3: Option[SqliteBool]): BoolTest =
68+
var boolTest: BoolTest
69+
db.exec(fmt"""INSERT INTO {boolTest.tableName} ({boolTest.testId.columnName}, {boolTest.boolCol1.columnName}, {boolTest.boolCol3.columnName}) VALUES (?, ?, ?);""", testId, bool1, bool3)
70+
db.getBoolRow(testId)
71+
72+
suite "sqlite_booleans":
73+
let password = "qwerty"
74+
let path = currentSourcePath.parentDir() & "/build/my.db"
75+
removeFile(path)
76+
let db = openDatabase(path)
77+
db.key(password)
78+
debugEcho "creating bool test table"
79+
db.createBoolTable()
80+
81+
test "using nim boolean types":
82+
let falseFalseNone1 = db.insertBoolRowBool("falseFalseNone1", false, false, bool.none)
83+
let falseTrueFalse1 = db.insertBoolRowBool("falseTrueFalse1", false, false.some)
84+
let trueFalseNone1 = db.insertBoolRowBool("trueFalseNone1", true, false, bool.none)
85+
let trueTrueTrue1 = db.insertBoolRowBool("trueTrueTrue1", true, true.some)
86+
87+
check:
88+
falseFalseNone1 == BoolTest(testId: "falseFalseNone1", boolCol1: false, boolCol2: false, boolCol3: bool.none)
89+
falseTrueFalse1 == BoolTest(testId: "falseTrueFalse1", boolCol1: false, boolCol2: true, boolCol3: false.some)
90+
trueFalseNone1 == BoolTest(testId: "trueFalseNone1", boolCol1: true, boolCol2: false, boolCol3: bool.none)
91+
trueTrueTrue1 == BoolTest(testId: "trueTrueTrue1", boolCol1: true, boolCol2: true, boolCol3: true.some)
92+
93+
test "using strings":
94+
let falseFalseNone2 = db.insertBoolRowString("falseFalseNone2", "false", "false", string.none)
95+
let falseTrueFalse2 = db.insertBoolRowString("falseTrueFalse2", "false", "false".some)
96+
let trueFalseNone2 = db.insertBoolRowString("trueFalseNone2", "true", "false", string.none)
97+
let trueTrueTrue2 = db.insertBoolRowString("trueTrueTrue2", "true", "true".some)
98+
99+
check:
100+
falseFalseNone2 == BoolTest(testId: "falseFalseNone2", boolCol1: false, boolCol2: false, boolCol3: bool.none)
101+
falseTrueFalse2 == BoolTest(testId: "falseTrueFalse2", boolCol1: false, boolCol2: true, boolCol3: false.some)
102+
trueFalseNone2 == BoolTest(testId: "trueFalseNone2", boolCol1: true, boolCol2: false, boolCol3: bool.none)
103+
trueTrueTrue2 == BoolTest(testId: "trueTrueTrue2", boolCol1: true, boolCol2: true, boolCol3: true.some)
104+
105+
106+
test "using uppercase strings":
107+
let falseFalseNone3 = db.insertBoolRowString("falseFalseNone3", "FALSE", "FALSE", string.none)
108+
let falseTrueFalse3 = db.insertBoolRowString("falseTrueFalse3", "FALSE", "FALSE".some)
109+
let trueFalseNone3 = db.insertBoolRowString("trueFalseNone3", "TRUE", "FALSE", string.none)
110+
let trueTrueTrue3 = db.insertBoolRowString("trueTrueTrue3", "TRUE", "TRUE".some)
111+
112+
check:
113+
falseFalseNone3 == BoolTest(testId: "falseFalseNone3", boolCol1: false, boolCol2: false, boolCol3: bool.none)
114+
falseTrueFalse3 == BoolTest(testId: "falseTrueFalse3", boolCol1: false, boolCol2: true, boolCol3: false.some)
115+
trueFalseNone3 == BoolTest(testId: "trueFalseNone3", boolCol1: true, boolCol2: false, boolCol3: bool.none)
116+
trueTrueTrue3 == BoolTest(testId: "trueTrueTrue3", boolCol1: true, boolCol2: true, boolCol3: true.some)
117+
118+
119+
test "using ints":
120+
let falseFalseNone4 = db.insertBoolRowInt("falseFalseNone4", 0, 0, int.none)
121+
let falseTrueFalse4 = db.insertBoolRowInt("falseTrueFalse4", 0, 0.some)
122+
let trueFalseNone4 = db.insertBoolRowInt("trueFalseNone4", 1, 0, int.none)
123+
let trueTrueTrue4 = db.insertBoolRowInt("trueTrueTrue4", 1, 1.some)
124+
125+
check:
126+
falseFalseNone4 == BoolTest(testId: "falseFalseNone4", boolCol1: false, boolCol2: false, boolCol3: bool.none)
127+
falseTrueFalse4 == BoolTest(testId: "falseTrueFalse4", boolCol1: false, boolCol2: true, boolCol3: false.some)
128+
trueFalseNone4 == BoolTest(testId: "trueFalseNone4", boolCol1: true, boolCol2: false, boolCol3: bool.none)
129+
trueTrueTrue4 == BoolTest(testId: "trueTrueTrue4", boolCol1: true, boolCol2: true, boolCol3: true.some)
130+
131+
test "using string ints":
132+
let falseFalseNone5 = db.insertBoolRowString("falseFalseNone5", "0", "0", string.none)
133+
let falseTrueFalse5 = db.insertBoolRowString("falseTrueFalse5", "0", "0".some)
134+
let trueFalseNone5 = db.insertBoolRowString("trueFalseNone5", "1", "0", string.none)
135+
let trueTrueTrue5 = db.insertBoolRowString("trueTrueTrue5", "1", "1".some)
136+
137+
check:
138+
falseFalseNone5 == BoolTest(testId: "falseFalseNone5", boolCol1: false, boolCol2: false, boolCol3: bool.none)
139+
falseTrueFalse5 == BoolTest(testId: "falseTrueFalse5", boolCol1: false, boolCol2: true, boolCol3: false.some)
140+
trueFalseNone5 == BoolTest(testId: "trueFalseNone5", boolCol1: true, boolCol2: false, boolCol3: bool.none)
141+
trueTrueTrue5 == BoolTest(testId: "trueTrueTrue5", boolCol1: true, boolCol2: true, boolCol3: true.some)
142+
143+
# Nim's standard lib strutils.parseBool supports yes/no and on/off, see
144+
# https://nim-lang.org/docs/strutils.html#parseBool%2Cstring for more information.
145+
# The following tests are just to illustrate support for these values, as well.
146+
147+
test "using yes/no":
148+
let falseFalseNone6 = db.insertBoolRowString("falseFalseNone6", "no", "no", string.none)
149+
let falseTrueFalse6 = db.insertBoolRowString("falseTrueFalse6", "no", "no".some)
150+
let trueFalseNone6 = db.insertBoolRowString("trueFalseNone6", "yes", "no", string.none)
151+
let trueTrueTrue6 = db.insertBoolRowString("trueTrueTrue6", "yes", "yes".some)
152+
153+
check:
154+
falseFalseNone6 == BoolTest(testId: "falseFalseNone6", boolCol1: false, boolCol2: false, boolCol3: bool.none)
155+
falseTrueFalse6 == BoolTest(testId: "falseTrueFalse6", boolCol1: false, boolCol2: true, boolCol3: false.some)
156+
trueFalseNone6 == BoolTest(testId: "trueFalseNone6", boolCol1: true, boolCol2: false, boolCol3: bool.none)
157+
trueTrueTrue6 == BoolTest(testId: "trueTrueTrue6", boolCol1: true, boolCol2: true, boolCol3: true.some)
158+
159+
test "using on/off":
160+
let falseFalseNone7 = db.insertBoolRowString("falseFalseNone7", "off", "off", string.none)
161+
let falseTrueFalse7 = db.insertBoolRowString("falseTrueFalse7", "off", "off".some)
162+
let trueFalseNone7 = db.insertBoolRowString("trueFalseNone7", "on", "off", string.none)
163+
let trueTrueTrue7 = db.insertBoolRowString("trueTrueTrue7", "on", "on".some)
164+
165+
check:
166+
falseFalseNone7 == BoolTest(testId: "falseFalseNone7", boolCol1: false, boolCol2: false, boolCol3: bool.none)
167+
falseTrueFalse7 == BoolTest(testId: "falseTrueFalse7", boolCol1: false, boolCol2: true, boolCol3: false.some)
168+
trueFalseNone7 == BoolTest(testId: "trueFalseNone7", boolCol1: true, boolCol2: false, boolCol3: bool.none)
169+
trueTrueTrue7 == BoolTest(testId: "trueTrueTrue7", boolCol1: true, boolCol2: true, boolCol3: true.some)
170+
171+
db.close()
172+
removeFile(path)

0 commit comments

Comments
 (0)