mirror of
https://github.com/logos-storage/nim-datastore.git
synced 2026-01-02 13:43:11 +00:00
587 lines
13 KiB
Nim
587 lines
13 KiB
Nim
import std/algorithm
|
|
import std/options
|
|
import std/os
|
|
|
|
import pkg/asynctest/unittest2
|
|
import pkg/chronos
|
|
import pkg/stew/results
|
|
|
|
import ../../datastore/sqlite_datastore
|
|
import ./templates
|
|
|
|
suite "SQLiteDatastore":
|
|
var
|
|
ds: SQLiteDatastore
|
|
|
|
# assumes tests/test_all is run from project root, e.g. with `nimble test`
|
|
let
|
|
basePath = "tests" / "test_data"
|
|
basePathAbs = getCurrentDir() / basePath
|
|
filename = "test_store" & dbExt
|
|
dbPathAbs = basePathAbs / filename
|
|
|
|
setup:
|
|
removeDir(basePathAbs)
|
|
require(not dirExists(basePathAbs))
|
|
createDir(basePathAbs)
|
|
|
|
teardown:
|
|
if not ds.isNil: ds.close
|
|
ds = nil
|
|
removeDir(basePathAbs)
|
|
require(not dirExists(basePathAbs))
|
|
|
|
asyncTest "new":
|
|
var
|
|
dsRes = SQLiteDatastore.new(basePathAbs, filename, readOnly = true)
|
|
|
|
# for `readOnly = true` to succeed the database file must already exist
|
|
check: dsRes.isErr
|
|
|
|
dsRes = SQLiteDatastore.new(basePathAbs / "missing", filename)
|
|
|
|
check: dsRes.isErr
|
|
|
|
dsRes = SQLiteDatastore.new(basePathAbs, filename)
|
|
|
|
check:
|
|
dsRes.isOk
|
|
fileExists(dbPathAbs)
|
|
|
|
dsRes.get.close
|
|
removeDir(basePathAbs)
|
|
assert not dirExists(basePathAbs)
|
|
createDir(basePathAbs)
|
|
|
|
dsRes = SQLiteDatastore.new(basePath, filename)
|
|
|
|
check:
|
|
dsRes.isOk
|
|
fileExists(dbPathAbs)
|
|
|
|
dsRes.get.close
|
|
|
|
# for `readOnly = true` to succeed the database file must already exist, so
|
|
# the existing file (per previous step) is not deleted prior to the next
|
|
# invocation of `SQLiteDatastore.new`
|
|
|
|
dsRes = SQLiteDatastore.new(basePath, filename, readOnly = true)
|
|
|
|
check: dsRes.isOk
|
|
|
|
dsRes.get.close
|
|
removeDir(basePathAbs)
|
|
assert not dirExists(basePathAbs)
|
|
createDir(basePathAbs)
|
|
|
|
dsRes = SQLiteDatastore.new(memory)
|
|
|
|
check: dsRes.isOk
|
|
|
|
dsRes.get.close
|
|
|
|
dsRes = SQLiteDatastore.new(memory, readOnly = true)
|
|
|
|
check: dsRes.isErr
|
|
|
|
asyncTest "accessors":
|
|
ds = SQLiteDatastore.new(basePath).get
|
|
|
|
check:
|
|
parentDir(ds.dbPath) == basePathAbs
|
|
not ds.env.isNil
|
|
|
|
asyncTest "helpers":
|
|
ds = SQLiteDatastore.new(basePath).get
|
|
|
|
ds.close
|
|
|
|
check:
|
|
ds.env.isNil
|
|
timestamp(10.123_456) == 10_123_456.int64
|
|
|
|
asyncTest "put":
|
|
let
|
|
key = Key.init("a:b/c/d:e").get
|
|
|
|
# for `readOnly = true` to succeed the database file must already exist
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
ds.close
|
|
ds = SQLiteDatastore.new(basePathAbs, filename, readOnly = true).get
|
|
|
|
var
|
|
bytes: seq[byte]
|
|
timestamp = timestamp()
|
|
putRes = await ds.put(key, bytes, timestamp)
|
|
|
|
check: putRes.isErr
|
|
|
|
ds.close
|
|
removeDir(basePathAbs)
|
|
assert not dirExists(basePathAbs)
|
|
createDir(basePathAbs)
|
|
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
|
|
timestamp = timestamp()
|
|
putRes = await ds.put(key, bytes, timestamp)
|
|
|
|
check: putRes.isOk
|
|
|
|
let
|
|
prequeryRes = NoParamsStmt.prepare(
|
|
ds.env, "SELECT timestamp AS foo, id AS baz, data AS bar FROM " &
|
|
tableName & ";")
|
|
|
|
assert prequeryRes.isOk
|
|
|
|
let
|
|
prequery = prequeryRes.get
|
|
idCol = idCol(RawStmtPtr(prequery), 1)
|
|
dataCol = dataCol(RawStmtPtr(prequery), 2)
|
|
timestampCol = timestampCol(RawStmtPtr(prequery), 0)
|
|
|
|
var
|
|
qId: string
|
|
qData: seq[byte]
|
|
qTimestamp: int64
|
|
rowCount = 0
|
|
|
|
proc onData(s: RawStmtPtr) {.closure.} =
|
|
qId = idCol()
|
|
qData = dataCol()
|
|
qTimestamp = timestampCol()
|
|
inc rowCount
|
|
|
|
var
|
|
qRes = prequery.query((), onData)
|
|
|
|
assert qRes.isOk
|
|
|
|
check:
|
|
qRes.get
|
|
qId == key.id
|
|
qData == bytes
|
|
qTimestamp == timestamp
|
|
rowCount == 1
|
|
|
|
bytes = @[1.byte, 2.byte, 3.byte]
|
|
timestamp = timestamp()
|
|
putRes = await ds.put(key, bytes, timestamp)
|
|
|
|
check: putRes.isOk
|
|
|
|
rowCount = 0
|
|
qRes = prequery.query((), onData)
|
|
assert qRes.isOk
|
|
|
|
check:
|
|
qRes.get
|
|
qId == key.id
|
|
qData == bytes
|
|
qTimestamp == timestamp
|
|
rowCount == 1
|
|
|
|
bytes = @[4.byte, 5.byte, 6.byte]
|
|
timestamp = timestamp()
|
|
putRes = await ds.put(key, bytes, timestamp)
|
|
|
|
check: putRes.isOk
|
|
|
|
rowCount = 0
|
|
qRes = prequery.query((), onData)
|
|
assert qRes.isOk
|
|
|
|
check:
|
|
qRes.get
|
|
qId == key.id
|
|
qData == bytes
|
|
qTimestamp == timestamp
|
|
rowCount == 1
|
|
|
|
prequery.dispose
|
|
|
|
asyncTest "delete":
|
|
let
|
|
bytes = @[1.byte, 2.byte, 3.byte]
|
|
|
|
var
|
|
key = Key.init("a:b/c/d:e").get
|
|
|
|
# for `readOnly = true` to succeed the database file must already exist
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
ds.close
|
|
ds = SQLiteDatastore.new(basePathAbs, filename, readOnly = true).get
|
|
|
|
var
|
|
delRes = await ds.delete(key)
|
|
|
|
check: delRes.isErr
|
|
|
|
ds.close
|
|
removeDir(basePathAbs)
|
|
assert not dirExists(basePathAbs)
|
|
createDir(basePathAbs)
|
|
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
|
|
let
|
|
putRes = await ds.put(key, bytes)
|
|
|
|
assert putRes.isOk
|
|
|
|
let
|
|
query = "SELECT * FROM " & tableName & ";"
|
|
|
|
var
|
|
rowCount = 0
|
|
|
|
proc onData(s: RawStmtPtr) {.closure.} =
|
|
inc rowCount
|
|
|
|
var
|
|
qRes = ds.env.query(query, onData)
|
|
|
|
assert qRes.isOk
|
|
check: rowCount == 1
|
|
delRes = await ds.delete(key)
|
|
|
|
check: delRes.isOk
|
|
|
|
rowCount = 0
|
|
qRes = ds.env.query(query, onData)
|
|
assert qRes.isOk
|
|
|
|
check:
|
|
delRes.isOk
|
|
rowCount == 0
|
|
|
|
key = Key.init("X/Y/Z").get
|
|
|
|
delRes = await ds.delete(key)
|
|
|
|
check: delRes.isOk
|
|
|
|
asyncTest "contains":
|
|
let
|
|
bytes = @[1.byte, 2.byte, 3.byte]
|
|
|
|
var
|
|
key = Key.init("a:b/c/d:e").get
|
|
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
|
|
let
|
|
putRes = await ds.put(key, bytes)
|
|
|
|
assert putRes.isOk
|
|
|
|
var
|
|
containsRes = await ds.contains(key)
|
|
|
|
check:
|
|
containsRes.isOk
|
|
containsRes.get == true
|
|
|
|
key = Key.init("X/Y/Z").get
|
|
|
|
containsRes = await ds.contains(key)
|
|
|
|
check:
|
|
containsRes.isOk
|
|
containsRes.get == false
|
|
|
|
asyncTest "get":
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
|
|
var
|
|
bytes: seq[byte]
|
|
key = Key.init("a:b/c/d:e").get
|
|
putRes = await ds.put(key, bytes)
|
|
|
|
assert putRes.isOk
|
|
|
|
var
|
|
getRes = await ds.get(key)
|
|
getOpt = getRes.get
|
|
|
|
check: getOpt.isSome and getOpt.get == bytes
|
|
|
|
bytes = @[1.byte, 2.byte, 3.byte]
|
|
putRes = await ds.put(key, bytes)
|
|
|
|
assert putRes.isOk
|
|
|
|
getRes = await ds.get(key)
|
|
getOpt = getRes.get
|
|
|
|
check: getOpt.isSome and getOpt.get == bytes
|
|
|
|
key = Key.init("X/Y/Z").get
|
|
|
|
assert not (await ds.contains(key)).get
|
|
|
|
getRes = await ds.get(key)
|
|
getOpt = getRes.get
|
|
|
|
check: getOpt.isNone
|
|
|
|
asyncTest "query":
|
|
ds = SQLiteDatastore.new(basePathAbs, filename).get
|
|
|
|
var
|
|
key1 = Key.init("a").get
|
|
key2 = Key.init("a/b").get
|
|
key3 = Key.init("a/b:c").get
|
|
key4 = Key.init("a:b").get
|
|
key5 = Key.init("a:b/c").get
|
|
key6 = Key.init("a:b/c:d").get
|
|
key7 = Key.init("A").get
|
|
key8 = Key.init("A/B").get
|
|
key9 = Key.init("A/B:C").get
|
|
key10 = Key.init("A:B").get
|
|
key11 = Key.init("A:B/C").get
|
|
key12 = Key.init("A:B/C:D").get
|
|
|
|
bytes1 = @[1.byte, 2.byte, 3.byte]
|
|
bytes2 = @[4.byte, 5.byte, 6.byte]
|
|
bytes3: seq[byte] = @[]
|
|
bytes4 = bytes1
|
|
bytes5 = bytes2
|
|
bytes6 = bytes3
|
|
bytes7 = bytes1
|
|
bytes8 = bytes2
|
|
bytes9 = bytes3
|
|
bytes10 = bytes1
|
|
bytes11 = bytes2
|
|
bytes12 = bytes3
|
|
|
|
queryKey = Key.init("*").get
|
|
|
|
var
|
|
putRes = await ds.put(key1, bytes1)
|
|
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key2, bytes2)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key3, bytes3)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key4, bytes4)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key5, bytes5)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key6, bytes6)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key7, bytes7)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key8, bytes8)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key9, bytes9)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key10, bytes10)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key11, bytes11)
|
|
assert putRes.isOk
|
|
putRes = await ds.put(key12, bytes12)
|
|
assert putRes.isOk
|
|
|
|
var
|
|
kds: seq[QueryResponse]
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
# see https://sqlite.org/lang_select.html#the_order_by_clause
|
|
# If a SELECT statement that returns more than one row does not have an
|
|
# ORDER BY clause, the order in which the rows are returned is undefined.
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key1, data: bytes1),
|
|
(key: key2, data: bytes2),
|
|
(key: key3, data: bytes3),
|
|
(key: key4, data: bytes4),
|
|
(key: key5, data: bytes5),
|
|
(key: key6, data: bytes6),
|
|
(key: key7, data: bytes7),
|
|
(key: key8, data: bytes8),
|
|
(key: key9, data: bytes9),
|
|
(key: key10, data: bytes10),
|
|
(key: key11, data: bytes11),
|
|
(key: key12, data: bytes12)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("a*").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key1, data: bytes1),
|
|
(key: key2, data: bytes2),
|
|
(key: key3, data: bytes3),
|
|
(key: key4, data: bytes4),
|
|
(key: key5, data: bytes5),
|
|
(key: key6, data: bytes6)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("A*").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key7, data: bytes7),
|
|
(key: key8, data: bytes8),
|
|
(key: key9, data: bytes9),
|
|
(key: key10, data: bytes10),
|
|
(key: key11, data: bytes11),
|
|
(key: key12, data: bytes12)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("a/?").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key2, data: bytes2)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("A/?").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key8, data: bytes8)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("*/?").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key2, data: bytes2),
|
|
(key: key5, data: bytes5),
|
|
(key: key8, data: bytes8),
|
|
(key: key11, data: bytes11)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("[Aa]/?").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key2, data: bytes2),
|
|
(key: key8, data: bytes8)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
# SQLite's GLOB operator, akin to Unix file globbing syntax, is greedy re:
|
|
# wildcard "*". So a pattern such as "a:*[^/]" will not restrict results to
|
|
# "/a:b", i.e. it will match on "/a:b", "/a:b/c" and "/a:b/c:d".
|
|
|
|
queryKey = Key.init("a:*[^/]").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key4, data: bytes4),
|
|
(key: key5, data: bytes5),
|
|
(key: key6, data: bytes6)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
queryKey = Key.init("a:*[Bb]").get
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds.sortedByIt(it.key.id) == @[
|
|
(key: key4, data: bytes4)
|
|
].sortedByIt(it.key.id)
|
|
|
|
kds = @[]
|
|
|
|
var
|
|
deleteRes = await ds.delete(key1)
|
|
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key2)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key3)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key4)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key5)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key6)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key7)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key8)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key9)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key10)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key11)
|
|
assert deleteRes.isOk
|
|
deleteRes = await ds.delete(key12)
|
|
assert deleteRes.isOk
|
|
|
|
let
|
|
emptyKds: seq[QueryResponse] = @[]
|
|
|
|
for kd in ds.query(Query.init(queryKey)):
|
|
let
|
|
(key, data) = await kd
|
|
|
|
kds.add (key, data)
|
|
|
|
check: kds == emptyKds
|