nim-leveldb/leveldbstatic.nim

506 lines
15 KiB
Nim
Raw Normal View History

2020-02-27 12:38:12 +00:00
## A LevelDB_ wrapper for Nim in a Nim friendly way.
##
## LevelDB is a fast and simple key/value data storage library built
## by Google that provides an ordered mapping from string keys to
## string values.
##
## .. _LevelDB: https://github.com/google/leveldb
##
## Create a database:
##
## .. code-block:: Nim
2024-05-13 12:08:17 +00:00
## import leveldbstatic
2020-02-27 12:38:12 +00:00
## import options
##
## var db = leveldb.open("/tmp/mydata")
##
## Read or modify the database content:
##
## .. code-block:: Nim
##
## assert db.getOrDefault("nothing", "") == ""
##
## db.put("hello", "world")
## db.put("bin", "GIF89a\1\0")
## echo db.get("hello")
## assert db.get("hello").isSome()
##
## var key, val = ""
## for key, val in db.iter():
## echo key, ": ", repr(val)
##
## db.delete("hello")
## assert db.get("hello").isNone()
##
## Batch writes:
##
## .. code-block:: Nim
## let batch = newBatch()
## for i in 1..10:
## batch.put("key" & $i, $i)
## batch.delete("bin")
## db.write(batch)
##
## Iterate over subset of database content:
##
## .. code-block:: Nim
## for key, val in db.iterPrefix(prefix = "key1"):
## echo key, ": ", val
## for key, val in db.iter(seek = "key3", reverse = true):
## echo key, ": ", val
##
## db.close()
import options, os, strutils
2024-05-13 12:08:17 +00:00
import leveldbstatic/raw
2016-05-17 19:28:34 +00:00
type
LevelDb* = ref object
2020-02-12 15:29:11 +00:00
path*: string
2016-05-17 19:28:34 +00:00
db: ptr leveldb_t
cache: ptr leveldb_cache_t
readOptions: ptr leveldb_readoptions_t
2016-05-17 19:28:34 +00:00
syncWriteOptions: ptr leveldb_writeoptions_t
asyncWriteOptions: ptr leveldb_writeoptions_t
LevelDbWriteBatch* = ref object
2020-02-27 12:38:12 +00:00
## Write batches for bulk data modification.
2019-12-14 10:11:20 +00:00
batch: ptr leveldb_writebatch_t
2020-02-12 15:29:32 +00:00
CompressionType* = enum
2020-02-27 12:38:12 +00:00
## No compression or using Snappy_ algorithm (default).
##
## .. _Snappy: http://google.github.io/snappy/
2020-02-12 15:29:32 +00:00
ctNoCompression = leveldb_no_compression,
ctSnappyCompression = leveldb_snappy_compression
2021-02-03 05:17:56 +00:00
LevelDbException* = object of CatchableError
2016-05-17 19:28:34 +00:00
2024-05-14 07:23:08 +00:00
IterNext* = proc(): (string, string) {.gcsafe, closure.}
2024-05-20 06:41:36 +00:00
IterDispose* = proc() {.gcsafe, closure.}
2024-05-14 07:23:08 +00:00
LevelDbQueryIter* = ref object
finished*: bool
next*: IterNext
2024-05-20 06:41:36 +00:00
dispose*: IterDispose
2024-05-14 07:23:08 +00:00
2019-11-06 15:48:35 +00:00
const
2020-02-19 11:11:11 +00:00
version* = block:
2024-05-13 12:08:17 +00:00
const configFile = "leveldbstatic.nimble"
const sourcePath = currentSourcePath()
const parentConfig = sourcePath.parentDir.parentDir / configFile
const localConfig = sourcePath.parentDir / configFile
var content: string
if fileExists(parentConfig):
content = staticRead(parentConfig)
else:
content = staticRead(localConfig)
2020-02-19 11:11:11 +00:00
var version_line: string
for line in content.split("\L"):
if line.startsWith("version"):
version_line = line
break
let raw = version_line.split("=", maxsplit = 1)[1]
raw.strip().strip(chars = {'"'})
levelDbTrue = uint8(1)
levelDbFalse = uint8(0)
2019-11-06 15:48:35 +00:00
2019-11-09 07:52:46 +00:00
proc free(p: pointer) {.importc.}
2016-05-17 19:28:34 +00:00
proc checkError(errPtr: cstring) =
if errPtr != nil:
defer: free(errPtr)
raise newException(LevelDbException, $errPtr)
proc getLibVersion*(): (int, int) =
2020-02-27 12:38:12 +00:00
## Get the version of leveldb C library.
result[0] = leveldb_major_version()
result[1] = leveldb_minor_version()
2016-05-17 19:28:34 +00:00
proc close*(self: LevelDb) =
2020-02-27 12:38:12 +00:00
## Closes the database.
##
## See also:
## * `open proc <#open%2Cstring%2Cint%2Cint%2Cint>`_
2016-05-17 19:28:34 +00:00
if self.db == nil:
2019-11-06 15:48:35 +00:00
return
2016-05-17 19:28:34 +00:00
leveldb_close(self.db)
leveldb_writeoptions_destroy(self.syncWriteOptions)
leveldb_writeoptions_destroy(self.asyncWriteOptions)
leveldb_readoptions_destroy(self.readOptions)
2019-12-14 10:11:56 +00:00
if self.cache != nil:
leveldb_cache_destroy(self.cache)
self.cache = nil
2016-05-17 19:28:34 +00:00
self.db = nil
2020-02-12 15:29:23 +00:00
proc open*(path: string, create = true, reuse = true, paranoidChecks = true,
2020-02-12 15:29:32 +00:00
compressionType = ctSnappyCompression,
2020-02-12 15:29:23 +00:00
cacheCapacity = 0, blockSize = 4 * 1024, writeBufferSize = 4*1024*1024,
maxOpenFiles = 1000, maxFileSize = 2 * 1024 * 1024,
2020-02-12 15:29:32 +00:00
blockRestartInterval = 16): LevelDb =
2020-02-27 12:38:12 +00:00
## Opens a database.
##
## Raises `LevelDbException` if corruption detected in the database.
##
## See also:
## * `close proc <#close%2CLevelDb>`_
2016-05-17 19:28:34 +00:00
new(result, close)
let options = leveldb_options_create()
defer: leveldb_options_destroy(options)
result.syncWriteOptions = leveldb_writeoptions_create()
2019-11-06 15:48:35 +00:00
leveldb_writeoptions_set_sync(result.syncWriteOptions, levelDbTrue)
2016-05-17 19:28:34 +00:00
result.asyncWriteOptions = leveldb_writeoptions_create()
2019-11-06 15:48:35 +00:00
leveldb_writeoptions_set_sync(result.asyncWriteOptions, levelDbFalse)
2016-05-17 19:28:34 +00:00
result.readOptions = leveldb_readoptions_create()
2020-02-12 15:29:23 +00:00
if create:
leveldb_options_set_create_if_missing(options, levelDbTrue)
else:
leveldb_options_set_create_if_missing(options, levelDbFalse)
if reuse:
leveldb_options_set_error_if_exists(options, levelDbFalse)
else:
leveldb_options_set_error_if_exists(options, levelDbTrue)
if paranoidChecks:
leveldb_options_set_paranoid_checks(options, levelDbTrue)
else:
leveldb_options_set_paranoid_checks(options, levelDbFalse)
2020-07-19 16:46:37 +00:00
leveldb_options_set_write_buffer_size(options, writeBufferSize.csize_t)
leveldb_options_set_block_size(options, blockSize.csize_t)
2020-02-12 15:29:23 +00:00
leveldb_options_set_max_open_files(options, cast[cint](maxOpenFiles))
2020-07-19 16:46:37 +00:00
leveldb_options_set_max_file_size(options, maxFileSize.csize_t)
2020-02-12 15:29:23 +00:00
leveldb_options_set_block_restart_interval(options,
2020-02-12 15:29:32 +00:00
cast[cint](blockRestartInterval))
leveldb_options_set_compression(options, cast[cint](compressionType.ord))
2020-02-12 15:29:23 +00:00
2019-12-14 10:11:56 +00:00
if cacheCapacity > 0:
2020-07-19 16:46:37 +00:00
let cache = leveldb_cache_create_lru(cacheCapacity.csize_t)
2019-12-14 10:11:56 +00:00
leveldb_options_set_cache(options, cache)
result.cache = cache
2016-05-17 19:28:34 +00:00
var errPtr: cstring = nil
2020-02-12 15:29:11 +00:00
result.path = path
2016-05-17 19:28:34 +00:00
result.db = leveldb_open(options, path, addr errPtr)
checkError(errPtr)
2020-02-27 12:34:11 +00:00
proc put*(self: LevelDb, key: string, value: string, sync = false) =
2020-02-27 12:38:12 +00:00
## Set a `value` for the specified `key`.
##
## By default, `sync` is turned off, each write to leveldb is asynchronous.
## Unless reboot, a crash of just the writing process will not cause any
## loss since even when `sync` is false.
##
## See also:
## * `put proc <#put%2CLevelDbWriteBatch%2Cstring%2Cstring>`_
runnableExamples:
let db = leveldb.open("/tmp/test")
db.put("hello", "world")
db.close()
2016-05-17 19:28:34 +00:00
assert self.db != nil
var errPtr: cstring = nil
let writeOptions = if sync: self.syncWriteOptions else: self.asyncWriteOptions
leveldb_put(self.db, writeOptions,
2020-07-19 16:46:37 +00:00
key, key.len.csize_t, value, value.len.csize_t, addr errPtr)
2016-05-17 19:28:34 +00:00
checkError(errPtr)
2020-07-19 16:46:37 +00:00
proc newString(cstr: cstring, length: csize_t): string =
2019-12-14 10:05:05 +00:00
if length > 0:
result = newString(length)
copyMem(unsafeAddr result[0], cstr, length)
else:
result = ""
2016-05-17 19:28:34 +00:00
proc get*(self: LevelDb, key: string): Option[string] =
2020-02-27 12:38:12 +00:00
## Get the value for the specified `key`.
##
## See also:
## * `getOrDefault proc <#getOrDefault%2CLevelDb%2Cstring>`_
runnableExamples:
let db = leveldb.open("/tmp/test")
db.put("hello", "world")
echo db.get("hello")
db.close()
2020-07-19 16:46:37 +00:00
var size: csize_t
2016-05-17 19:28:34 +00:00
var errPtr: cstring = nil
2020-07-19 16:46:37 +00:00
let s = leveldb_get(self.db, self.readOptions, key, key.len.csize_t, addr size, addr errPtr)
2016-05-17 19:28:34 +00:00
checkError(errPtr)
if s == nil:
result = none(string)
else:
2019-12-14 10:05:05 +00:00
result = some(newString(s, size))
2016-05-17 19:28:34 +00:00
free(s)
2020-02-27 12:31:06 +00:00
proc getOrDefault*(self: LevelDb, key: string, default = ""): string =
2020-02-27 12:38:12 +00:00
## Get the value for the specified `key`, or `default` if no value was set.
##
## See also:
## * `get proc <#get%2CLevelDb%2Cstring>`_
2020-02-27 12:31:06 +00:00
runnableExamples:
let db = leveldb.open("/tmp/test")
doAssert db.getOrDefault("what?", "nothing") == "nothing"
db.close()
let val = self.get(key)
if val.isNone():
result = default
else:
result = val.get()
2020-02-27 12:34:11 +00:00
proc delete*(self: LevelDb, key: string, sync = false) =
2020-02-27 12:38:12 +00:00
## Delete the key/value pair for the specified key.
##
## See also:
## * `delete proc <#delete%2CLevelDbWriteBatch%2Cstring>`_
2019-11-06 15:48:35 +00:00
var errPtr: cstring = nil
let writeOptions = if sync: self.syncWriteOptions else: self.asyncWriteOptions
2020-07-19 16:46:37 +00:00
leveldb_delete(self.db, writeOptions, key, key.len.csize_t, addr errPtr)
2019-11-06 15:48:35 +00:00
checkError(errPtr)
proc destroy*(self: LevelDbWriteBatch) =
2020-02-27 12:38:12 +00:00
## Destroys this batch.
##
## See also:
## * `newBatch proc <#newBatch>`_
2019-12-14 10:11:20 +00:00
if self.batch == nil:
return
leveldb_writebatch_destroy(self.batch)
self.batch = nil
proc newBatch*(): LevelDbWriteBatch =
2020-02-27 12:38:12 +00:00
## Creates a new database write batch.
##
## See also:
## * `write proc <#write%2CLevelDb%2CLevelDbWriteBatch>`_
## * `put proc <#put%2CLevelDbWriteBatch%2Cstring%2Cstring>`_
## * `delete proc <#delete%2CLevelDbWriteBatch%2Cstring>`_
## * `append proc <#append%2CLevelDbWriteBatch%2CLevelDbWriteBatch>`_
## * `clear proc <#clear%2CLevelDbWriteBatch>`_
## * `destroy proc <#destroy%2CLevelDbWriteBatch>`_
runnableExamples:
let db = leveldb.open("/tmp/test")
let batch = newBatch()
for i in 1..10:
batch.put("key" & $i, $i)
batch.delete("another")
db.write(batch)
db.close()
2019-12-14 10:11:20 +00:00
new(result, destroy)
result.batch = leveldb_writebatch_create()
2020-02-27 12:34:11 +00:00
proc put*(self: LevelDbWriteBatch, key: string, value: string, sync = false) =
2020-02-27 12:38:12 +00:00
## Set a `value` for the specified `key`.
## Same as `put <#put%2CLevelDb%2Cstring%2Cstring>`_ but operates on the
## write batch instead.
##
## See also:
## * `put proc <#put%2CLevelDb%2Cstring%2Cstring>`_
## * `newBatch proc <#newBatch>`_
2020-07-19 16:46:37 +00:00
leveldb_writebatch_put(self.batch, key, key.len.csize_t, value, value.len.csize_t)
2019-12-14 10:11:20 +00:00
proc append*(self, source: LevelDbWriteBatch) =
2020-02-27 12:38:12 +00:00
## Merges the `source` batch into this batch.
##
## See also:
## * `newBatch proc <#newBatch>`_
2019-12-14 10:11:20 +00:00
leveldb_writebatch_append(self.batch, source.batch)
proc delete*(self: LevelDbWriteBatch, key: string) =
2020-02-27 12:38:12 +00:00
## Delete the key/value pair for the specified `key`.
## Same as `delete <#delete%2CLevelDb%2Cstring>`_ but operates on the
## write batch instead.
##
## See also:
## * `delete proc <#delete%2CLevelDb%2Cstring>`_
## * `newBatch proc <#newBatch>`_
2020-07-19 16:46:37 +00:00
leveldb_writebatch_delete(self.batch, key, key.len.csize_t)
2019-12-14 10:11:20 +00:00
proc clear*(self: LevelDbWriteBatch) =
2020-02-27 12:38:12 +00:00
## Clear all updates buffered in this batch.
##
## See also:
## * `newBatch proc <#newBatch>`_
2019-12-14 10:11:20 +00:00
leveldb_writebatch_clear(self.batch)
proc write*(self: LevelDb, batch: LevelDbWriteBatch) =
2020-02-27 12:38:12 +00:00
## Write apply the given `batch` to the database.
##
## See also:
## * `newBatch proc <#newBatch>`_
2019-12-14 10:11:20 +00:00
var errPtr: cstring = nil
leveldb_write(self.db, self.syncWriteOptions, batch.batch, addr errPtr)
checkError(errPtr)
2019-12-14 10:05:05 +00:00
proc getIterData(iterPtr: ptr leveldb_iterator_t): (string, string) =
2020-07-19 16:46:37 +00:00
var len: csize_t
2019-11-06 15:48:35 +00:00
var str: cstring
str = leveldb_iter_key(iterPtr, addr len)
2019-12-14 10:05:05 +00:00
result[0] = newString(str, len)
2019-11-06 15:48:35 +00:00
str = leveldb_iter_value(iterPtr, addr len)
2019-12-14 10:05:05 +00:00
result[1] = newString(str, len)
2019-11-06 15:48:35 +00:00
iterator iter*(self: LevelDb, seek: string = "", reverse: bool = false): (
string, string) =
2020-02-27 12:38:12 +00:00
## Iterate all key/value pairs in the database from the first one or
## the specified key `seek`.
## By default, the ordering will be lexicographic byte-wise ordering
## with leveldb builtin comparator, unless `reverse` set to `true`.
##
## See also:
## * `iterPrefix iterator <#iterPrefix.i%2CLevelDb%2Cstring>`_
## * `iterRange iterator <#iterRange.i%2CLevelDb%2Cstring%2Cstring>`_
2019-11-06 15:48:35 +00:00
var iterPtr = leveldb_create_iterator(self.db, self.readOptions)
defer: leveldb_iter_destroy(iterPtr)
if seek.len > 0:
2020-07-19 16:46:37 +00:00
leveldb_iter_seek(iterPtr, seek, seek.len.csize_t)
2019-11-06 15:48:35 +00:00
else:
if reverse:
leveldb_iter_seek_to_last(iterPtr)
else:
leveldb_iter_seek_to_first(iterPtr)
while true:
if leveldb_iter_valid(iterPtr) == levelDbFalse:
break
var (key, value) = getIterData(iterPtr)
var err: cstring = nil
leveldb_iter_get_error(iterPtr, addr err)
checkError(err)
2019-12-14 10:05:05 +00:00
yield (key, value)
2019-11-06 15:48:35 +00:00
if reverse:
leveldb_iter_prev(iterPtr)
else:
leveldb_iter_next(iterPtr)
2019-11-16 09:42:48 +00:00
iterator iterPrefix*(self: LevelDb, prefix: string): (string, string) =
2020-02-27 12:38:12 +00:00
## Iterate subset key/value pairs in the database with a particular `prefix`.
##
## See also:
## * `iter iterator <#iter.i%2CLevelDb%2Cstring%2Cbool>`_
## * `iterRange iterator <#iterRange.i%2CLevelDb%2Cstring%2Cstring>`_
2020-02-18 15:46:17 +00:00
for key, value in iter(self, prefix, reverse = false):
2019-11-16 09:42:40 +00:00
if key.startsWith(prefix):
yield (key, value)
2019-11-16 11:35:02 +00:00
else:
break
2019-11-16 09:42:40 +00:00
2019-11-16 09:42:48 +00:00
iterator iterRange*(self: LevelDb, start, limit: string): (string, string) =
2020-02-27 12:38:12 +00:00
## Yields all key/value pairs between the `start` and `limit` keys
## (inclusive) in the database.
##
## See also:
## * `iter iterator <#iter.i%2CLevelDb%2Cstring%2Cbool>`_
## * `iterPrefix iterator <#iterPrefix.i%2CLevelDb%2Cstring>`_
2019-11-16 09:42:48 +00:00
let reverse: bool = limit < start
2020-02-18 15:46:17 +00:00
for key, value in iter(self, start, reverse = reverse):
2019-11-16 09:42:48 +00:00
if reverse:
if key < limit:
break
else:
if key > limit:
break
yield (key, value)
2024-05-14 07:23:08 +00:00
proc getIterKey(iterPtr: ptr leveldb_iterator_t): string =
var len: csize_t
var str: cstring
str = leveldb_iter_key(iterPtr, addr len)
return newString(str, len)
proc getIterValue(iterPtr: ptr leveldb_iterator_t): string =
var len: csize_t
var str: cstring
str = leveldb_iter_value(iterPtr, addr len)
return newString(str, len)
proc seekToQueryStart(iterPtr: ptr leveldb_iterator_t, prefix: string, skip: int) =
2024-05-14 07:23:08 +00:00
if prefix.len > 0:
leveldb_iter_seek(iterPtr, prefix, prefix.len.csize_t)
else:
leveldb_iter_seek_to_first(iterPtr)
for i in 0..<skip:
leveldb_iter_next(iterPtr)
proc closeIter(iter: LevelDbQueryIter, iterPtr: ptr leveldb_iterator_t) =
iter.finished = true
leveldb_iter_destroy(iterPtr)
proc queryIter*(self: LevelDb, prefix: string = "", keysOnly: bool = false, skip: int = 0, limit: int = 0): LevelDbQueryIter =
var iterPtr = leveldb_create_iterator(self.db, self.readOptions)
2024-05-14 07:23:08 +00:00
seekToQueryStart(iterPtr, prefix, skip)
var
iter = LevelDbQueryIter()
remaining = limit
2024-05-14 07:23:08 +00:00
let emptyResponse = ("", "")
proc getNext(): (string, string) {.gcsafe, closure.} =
if iter.finished:
return emptyResponse
if leveldb_iter_valid(iterPtr) == levelDbFalse or (limit > 0 and remaining == 0):
iter.closeIter(iterPtr)
2024-05-14 07:23:08 +00:00
return emptyResponse
if limit > 0:
dec remaining
2024-05-14 07:23:08 +00:00
let
keyStr = getIterKey(iterPtr)
valueStr = if keysOnly: "" else: getIterValue(iterPtr)
var err: cstring = nil
leveldb_iter_get_error(iterPtr, addr err)
checkError(err)
leveldb_iter_next(iterPtr)
if prefix.len > 0:
if keyStr.startsWith(prefix):
return (keyStr, valueStr)
else:
iter.closeIter(iterPtr)
2024-05-14 07:23:08 +00:00
return emptyResponse
else:
return (keyStr, valueStr)
2024-05-20 06:41:36 +00:00
proc dispose() {.gcsafe, closure.} =
if not iter.finished:
iter.closeIter(iterPtr)
2024-05-14 07:23:08 +00:00
iter.finished = false
iter.next = getNext
2024-05-20 06:41:36 +00:00
iter.dispose = dispose
2024-05-14 07:23:08 +00:00
return iter
proc removeDb*(name: string) =
2020-02-27 12:38:12 +00:00
## Remove the database `name`.
var err: cstring = nil
let options = leveldb_options_create()
leveldb_destroy_db(options, name, addr err)
checkError(err)
2020-02-12 15:29:11 +00:00
proc repairDb*(name: string) =
2020-02-27 12:38:12 +00:00
## Repairs the corrupted database `name`.
let options = leveldb_options_create()
2020-02-12 15:29:23 +00:00
leveldb_options_set_create_if_missing(options, levelDbFalse)
leveldb_options_set_error_if_exists(options, levelDbFalse)
var errPtr: cstring = nil
leveldb_repair_db(options, name, addr errPtr)
checkError(errPtr)