164 lines
4.8 KiB
Nim
164 lines
4.8 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2022-2023 Status Research & Development GmbH
|
|
# Licensed and distributed under either of
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
{.push raises: [].}
|
|
|
|
import
|
|
std/[options, os],
|
|
strutils,
|
|
eth/db/kvstore,
|
|
eth/db/kvstore_sqlite3,
|
|
stint
|
|
|
|
export kvstore_sqlite3
|
|
|
|
type
|
|
ContentData = tuple
|
|
contentId: array[32, byte]
|
|
contentKey: seq[byte]
|
|
content: seq[byte]
|
|
|
|
ContentDataDist* = tuple
|
|
contentId: array[32, byte]
|
|
contentKey: seq[byte]
|
|
content: seq[byte]
|
|
distance: array[32, byte]
|
|
|
|
SeedDb* = ref object
|
|
store: SqStoreRef
|
|
putStmt: SqliteStmt[(array[32, byte], seq[byte], seq[byte]), void]
|
|
getStmt: SqliteStmt[array[32, byte], ContentData]
|
|
getInRangeStmt: SqliteStmt[(array[32, byte], array[32, byte], int64, int64), ContentDataDist]
|
|
|
|
func xorDistance(
|
|
a: openArray[byte],
|
|
b: openArray[byte]
|
|
): Result[seq[byte], cstring] {.cdecl.} =
|
|
var s: seq[byte] = newSeq[byte](32)
|
|
|
|
if len(a) != 32 or len(b) != 32:
|
|
return err("Blobs should have 32 byte length")
|
|
|
|
var i = 0
|
|
while i < 32:
|
|
s[i] = a[i] xor b[i]
|
|
inc i
|
|
|
|
return ok(s)
|
|
|
|
template expectDb(x: auto): untyped =
|
|
# There's no meaningful error handling implemented for a corrupt database or
|
|
# full disk - this requires manual intervention, so we'll panic for now
|
|
x.expect("working database (disk broken/full?)")
|
|
|
|
proc getDbBasePathAndName*(path: string): Option[(string, string)] =
|
|
let (basePath, name) = splitPath(path)
|
|
if len(basePath) > 0 and len(name) > 0 and name.endsWith(".sqlite3"):
|
|
let nameAndExt = rsplit(name, ".", 1)
|
|
|
|
if len(nameAndExt) < 2 and len(nameAndExt[0]) == 0:
|
|
return none((string, string))
|
|
|
|
return some((basePath, nameAndExt[0]))
|
|
else:
|
|
return none((string, string))
|
|
|
|
proc new*(T: type SeedDb, path: string, name: string, inMemory = false): SeedDb =
|
|
let db =
|
|
if inMemory:
|
|
SqStoreRef.init("", "seed-db-test", inMemory = true).expect(
|
|
"working database (out of memory?)")
|
|
else:
|
|
SqStoreRef.init(path, name).expectDb()
|
|
|
|
if not db.readOnly:
|
|
let createSql = """
|
|
CREATE TABLE IF NOT EXISTS seed_data (
|
|
contentid BLOB PRIMARY KEY,
|
|
contentkey BLOB,
|
|
content BLOB
|
|
);"""
|
|
|
|
db.exec(createSql).expectDb()
|
|
|
|
let putStmt =
|
|
db.prepareStmt(
|
|
"INSERT OR REPLACE INTO seed_data (contentid, contentkey, content) VALUES (?, ?, ?);",
|
|
(array[32, byte], seq[byte], seq[byte]),
|
|
void).get()
|
|
|
|
let getStmt =
|
|
db.prepareStmt(
|
|
"SELECT contentid, contentkey, content FROM seed_data WHERE contentid = ?;",
|
|
array[32, byte],
|
|
ContentData
|
|
).get()
|
|
|
|
db.registerCustomScalarFunction("xorDistance", xorDistance)
|
|
.expect("Couldn't register custom xor function")
|
|
|
|
let getInRangeStmt =
|
|
db.prepareStmt(
|
|
"""
|
|
SELECT contentid, contentkey, content, xorDistance(?, contentid) as distance
|
|
FROM seed_data
|
|
WHERE distance <= ?
|
|
LIMIT ?
|
|
OFFSET ?;
|
|
""",
|
|
(array[32, byte], array[32, byte], int64, int64),
|
|
ContentDataDist
|
|
).get()
|
|
|
|
SeedDb(
|
|
store: db,
|
|
putStmt: putStmt,
|
|
getStmt: getStmt,
|
|
getInRangeStmt: getInRangeStmt
|
|
)
|
|
|
|
proc put*(db: SeedDb, contentId: array[32, byte], contentKey: seq[byte], content: seq[byte]): void =
|
|
db.putStmt.exec((contentId, contentKey, content)).expectDb()
|
|
|
|
proc put*(db: SeedDb, contentId: UInt256, contentKey: seq[byte], content: seq[byte]): void =
|
|
db.put(contentId.toBytesBE(), contentKey, content)
|
|
|
|
proc get*(db: SeedDb, contentId: array[32, byte]): Option[ContentData] =
|
|
var res = none[ContentData]()
|
|
discard db.getStmt.exec(contentId, proc (v: ContentData) = res = some(v)).expectDb()
|
|
return res
|
|
|
|
proc get*(db: SeedDb, contentId: UInt256): Option[ContentData] =
|
|
db.get(contentId.toBytesBE())
|
|
|
|
proc getContentInRange*(
|
|
db: SeedDb,
|
|
nodeId: UInt256,
|
|
nodeRadius: UInt256,
|
|
max: int64,
|
|
offset: int64): seq[ContentDataDist] =
|
|
## Return `max` amount of content in `nodeId` range, starting from `offset` position
|
|
## i.e using `offset` 0 will return `max` closest items, using `offset` `10` will
|
|
## will retrun `max` closest items except first 10
|
|
|
|
var res: seq[ContentDataDist] = @[]
|
|
var cd: ContentDataDist
|
|
for e in db.getInRangeStmt.exec((nodeId.toBytesBE(), nodeRadius.toBytesBE(), max, offset), cd):
|
|
res.add(cd)
|
|
return res
|
|
|
|
proc getContentInRange*(
|
|
db: SeedDb,
|
|
nodeId: UInt256,
|
|
nodeRadius: UInt256,
|
|
max: int64): seq[ContentDataDist] =
|
|
## Return `max` amount of content in `nodeId` range, starting from closest content
|
|
return db.getContentInRange(nodeId, nodeRadius, max, 0)
|
|
|
|
proc close*(db: SeedDb) =
|
|
db.store.close()
|