cryptodb 2.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cryptodb-2.4.3.dist-info/METADATA +61 -0
- cryptodb-2.4.3.dist-info/RECORD +27 -0
- cryptodb-2.4.3.dist-info/WHEEL +4 -0
- cryptodb-2.4.3.dist-info/entry_points.txt +2 -0
- cryptodb-2.4.3.dist-info/licenses/LICENSE +65 -0
- nedb/__init__.py +92 -0
- nedb/autoindex.py +142 -0
- nedb/backends/__init__.py +0 -0
- nedb/backends/redis_backend.py +115 -0
- nedb/cascade.py +130 -0
- nedb/concurrent.py +218 -0
- nedb/crypto.py +294 -0
- nedb/engine.py +783 -0
- nedb/index.py +98 -0
- nedb/log.py +216 -0
- nedb/merkle.py +62 -0
- nedb/mongo.py +824 -0
- nedb/proof.py +126 -0
- nedb/query.py +305 -0
- nedb/redis_compat.py +516 -0
- nedb/relations.py +51 -0
- nedb/resp2.py +250 -0
- nedb/server.py +1011 -0
- nedb/snapshot.py +216 -0
- nedb/sql.py +430 -0
- nedb/store.py +68 -0
- nedb/wrap_redis.py +725 -0
nedb/redis_compat.py
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nedb.redis_compat — Redis command compatibility adapter.
|
|
3
|
+
|
|
4
|
+
Maps the Redis command surface deterministically to NEDB primitives. No Redis
|
|
5
|
+
or hiredis code is used or required — Redis commands are a familiar entry point;
|
|
6
|
+
NEDB executes everything natively using its append-only log, MVCC, and relations.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from nedb import NEDB
|
|
11
|
+
from nedb.redis_compat import RedisCompat
|
|
12
|
+
|
|
13
|
+
db = NEDB("./data")
|
|
14
|
+
r = RedisCompat(db)
|
|
15
|
+
|
|
16
|
+
r.execute("SET", "mykey", "hello") # → "OK"
|
|
17
|
+
r.execute("GET", "mykey") # → "hello"
|
|
18
|
+
r.execute("HSET", "user:1", "name", "Ada", "age", "31")
|
|
19
|
+
r.execute("HGETALL", "user:1") # → {"name": "Ada", "age": "31"}
|
|
20
|
+
r.execute("SADD", "tags", "python", "rust")
|
|
21
|
+
r.execute("SMEMBERS", "tags") # → {"python", "rust"}
|
|
22
|
+
|
|
23
|
+
Key → NEDB mapping
|
|
24
|
+
──────────────────
|
|
25
|
+
Strings (SET/GET) → collection "_kv", id = key, doc = {"_v": value}
|
|
26
|
+
Hashes (HSET/…) → collection = key, id = field, doc = {"_v": value}
|
|
27
|
+
Sets (SADD/…) → relation edges from "_set:<key>" to "_smember:<key>:<member>"
|
|
28
|
+
Lists (LPUSH/…) → collection "_list:<key>", auto-seq id, doc = {"_v", "_seq"}
|
|
29
|
+
|
|
30
|
+
Unsupported commands (todo):
|
|
31
|
+
EXPIRE, TTL, PEXPIRE, PTTL, PERSIST — no built-in TTL mechanism
|
|
32
|
+
SUBSCRIBE, PUBLISH, UNSUBSCRIBE — no pub-sub layer
|
|
33
|
+
MULTI, EXEC, DISCARD, WATCH — no transaction isolation
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import re
|
|
38
|
+
import time
|
|
39
|
+
import uuid
|
|
40
|
+
from typing import Any, Dict, List, Optional, Set
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _safe(name: str) -> str:
|
|
44
|
+
"""Encode a Redis key into a NQL-safe NEDB collection name.
|
|
45
|
+
Characters outside [A-Za-z0-9_] are replaced with __XX__ (hex).
|
|
46
|
+
This is deterministic and collision-free — 'user:1' → 'user__3a__1'.
|
|
47
|
+
"""
|
|
48
|
+
return re.sub(r"[^A-Za-z0-9_]", lambda m: f"__{ord(m.group()):02x}__", name)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
_KV_COLL = "_kv"
|
|
52
|
+
_LIST_PREFIX = "_list_"
|
|
53
|
+
_SET_NODE_PREFIX = "_set_"
|
|
54
|
+
_SET_MEMBER_PREFIX = "_smember_"
|
|
55
|
+
|
|
56
|
+
UNSUPPORTED = {
|
|
57
|
+
"EXPIRE", "EXPIREAT", "EXPIRETIME", "PEXPIRE", "PEXPIREAT",
|
|
58
|
+
"TTL", "PTTL", "PERSIST",
|
|
59
|
+
"SUBSCRIBE", "UNSUBSCRIBE", "PSUBSCRIBE", "PUNSUBSCRIBE",
|
|
60
|
+
"PUBLISH", "PUBSUB",
|
|
61
|
+
"MULTI", "EXEC", "DISCARD", "WATCH", "UNWATCH",
|
|
62
|
+
"WAIT", "OBJECT", "DEBUG", "MONITOR", "SLOWLOG",
|
|
63
|
+
"CLUSTER", "REPLICAOF", "SLAVEOF", "BGSAVE", "BGREWRITEAOF",
|
|
64
|
+
"LASTSAVE", "SAVE", "RESTORE", "DUMP", "MIGRATE", "MOVE",
|
|
65
|
+
"SORT", "EVAL", "EVALSHA", "SCRIPT",
|
|
66
|
+
"GEORADIUSBYMEMBER", "GEOADD", "GEODIST", "GEOPOS",
|
|
67
|
+
"XADD", "XREAD", "XLEN", "XRANGE",
|
|
68
|
+
"BF.ADD", "CF.ADD",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_TODO_REASON = {
|
|
72
|
+
"EXPIRE": "TTL/expiry is on the NEDB roadmap. Track: github.com/Eth-Interchained/nedb/issues.",
|
|
73
|
+
"TTL": "TTL is on the NEDB roadmap.",
|
|
74
|
+
"PTTL": "TTL is on the NEDB roadmap.",
|
|
75
|
+
"SUBSCRIBE": "Pub-sub is on the NEDB roadmap.",
|
|
76
|
+
"PUBLISH": "Pub-sub is on the NEDB roadmap.",
|
|
77
|
+
"MULTI": "Transactions (MULTI/EXEC) are on the NEDB roadmap.",
|
|
78
|
+
"EXEC": "Transactions (MULTI/EXEC) are on the NEDB roadmap.",
|
|
79
|
+
"DISCARD": "Transactions are on the NEDB roadmap.",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RedisUnsupportedError(Exception):
|
|
84
|
+
"""Raised when a Redis command is not yet implemented in NEDB."""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class RedisError(Exception):
|
|
88
|
+
"""Raised on a Redis-compatible argument or usage error."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class RedisCompat:
|
|
92
|
+
"""
|
|
93
|
+
Redis-compatible command interface over a NEDB database.
|
|
94
|
+
|
|
95
|
+
Each command is executed transactionally (append-only log) with NEDB's
|
|
96
|
+
replay-protection. Pass ``client`` to scope nonce counters per service.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, db: Any, client: str = "redis-compat"):
|
|
100
|
+
self._db = db
|
|
101
|
+
self._client = client
|
|
102
|
+
|
|
103
|
+
# ── dispatch ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
def execute(self, command: str, *args: Any) -> Any:
|
|
106
|
+
"""
|
|
107
|
+
Execute a Redis command and return the result.
|
|
108
|
+
|
|
109
|
+
Arguments mirror the Redis protocol — all string, no type coercion
|
|
110
|
+
except what Redis itself performs (INCR coerces to int, HSET takes
|
|
111
|
+
alternating field/value pairs, etc.).
|
|
112
|
+
"""
|
|
113
|
+
cmd = command.upper()
|
|
114
|
+
|
|
115
|
+
if cmd in UNSUPPORTED:
|
|
116
|
+
reason = _TODO_REASON.get(cmd, "Not yet implemented in NEDB.")
|
|
117
|
+
raise RedisUnsupportedError(
|
|
118
|
+
f"{cmd} is not supported. {reason}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# ── String commands ────────────────────────────────────────────────
|
|
122
|
+
if cmd == "PING":
|
|
123
|
+
return "PONG" if not args else args[0]
|
|
124
|
+
if cmd == "SET":
|
|
125
|
+
return self._set(*args)
|
|
126
|
+
if cmd == "GET":
|
|
127
|
+
return self._get(*args)
|
|
128
|
+
if cmd == "GETDEL":
|
|
129
|
+
return self._getdel(*args)
|
|
130
|
+
if cmd == "SETNX":
|
|
131
|
+
return self._setnx(*args)
|
|
132
|
+
if cmd == "MSET":
|
|
133
|
+
return self._mset(*args)
|
|
134
|
+
if cmd == "MGET":
|
|
135
|
+
return self._mget(*args)
|
|
136
|
+
if cmd == "DEL":
|
|
137
|
+
return self._del(*args)
|
|
138
|
+
if cmd == "UNLINK":
|
|
139
|
+
return self._del(*args) # same semantics for our purposes
|
|
140
|
+
if cmd == "EXISTS":
|
|
141
|
+
return self._exists(*args)
|
|
142
|
+
if cmd == "INCR":
|
|
143
|
+
return self._incrby(args[0], 1)
|
|
144
|
+
if cmd == "INCRBY":
|
|
145
|
+
return self._incrby(args[0], int(args[1]))
|
|
146
|
+
if cmd == "DECR":
|
|
147
|
+
return self._incrby(args[0], -1)
|
|
148
|
+
if cmd == "DECRBY":
|
|
149
|
+
return self._incrby(args[0], -int(args[1]))
|
|
150
|
+
if cmd == "APPEND":
|
|
151
|
+
return self._append(*args)
|
|
152
|
+
if cmd == "STRLEN":
|
|
153
|
+
v = self._get(args[0])
|
|
154
|
+
return len(str(v)) if v is not None else 0
|
|
155
|
+
if cmd == "TYPE":
|
|
156
|
+
return self._type(*args)
|
|
157
|
+
if cmd == "RENAME":
|
|
158
|
+
return self._rename(*args)
|
|
159
|
+
if cmd == "KEYS":
|
|
160
|
+
return self._keys(args[0] if args else "*")
|
|
161
|
+
if cmd == "DBSIZE":
|
|
162
|
+
return len(self._keys("*"))
|
|
163
|
+
if cmd == "FLUSHDB":
|
|
164
|
+
return self._flushdb()
|
|
165
|
+
|
|
166
|
+
# ── Hash commands ──────────────────────────────────────────────────
|
|
167
|
+
if cmd == "HSET":
|
|
168
|
+
return self._hset(*args)
|
|
169
|
+
if cmd == "HMSET":
|
|
170
|
+
self._hset(*args); return "OK"
|
|
171
|
+
if cmd == "HSETNX":
|
|
172
|
+
return self._hsetnx(*args)
|
|
173
|
+
if cmd == "HGET":
|
|
174
|
+
return self._hget(*args)
|
|
175
|
+
if cmd == "HMGET":
|
|
176
|
+
return self._hmget(*args)
|
|
177
|
+
if cmd == "HGETALL":
|
|
178
|
+
return self._hgetall(*args)
|
|
179
|
+
if cmd == "HDEL":
|
|
180
|
+
return self._hdel(*args)
|
|
181
|
+
if cmd == "HEXISTS":
|
|
182
|
+
return self._hexists(*args)
|
|
183
|
+
if cmd == "HKEYS":
|
|
184
|
+
return self._hkeys(*args)
|
|
185
|
+
if cmd == "HVALS":
|
|
186
|
+
return self._hvals(*args)
|
|
187
|
+
if cmd == "HLEN":
|
|
188
|
+
return self._hlen(*args)
|
|
189
|
+
if cmd == "HINCRBY":
|
|
190
|
+
return self._hincrby(*args)
|
|
191
|
+
|
|
192
|
+
# ── Set commands ───────────────────────────────────────────────────
|
|
193
|
+
if cmd == "SADD":
|
|
194
|
+
return self._sadd(*args)
|
|
195
|
+
if cmd == "SMEMBERS":
|
|
196
|
+
return self._smembers(*args)
|
|
197
|
+
if cmd == "SISMEMBER":
|
|
198
|
+
return self._sismember(*args)
|
|
199
|
+
if cmd == "SREM":
|
|
200
|
+
return self._srem(*args)
|
|
201
|
+
if cmd == "SCARD":
|
|
202
|
+
return self._scard(*args)
|
|
203
|
+
if cmd == "SUNION":
|
|
204
|
+
result: Set[str] = set()
|
|
205
|
+
for k in args:
|
|
206
|
+
result |= self._smembers(k)
|
|
207
|
+
return result
|
|
208
|
+
if cmd == "SINTER":
|
|
209
|
+
sets = [self._smembers(k) for k in args]
|
|
210
|
+
return sets[0].intersection(*sets[1:]) if sets else set()
|
|
211
|
+
if cmd == "SDIFF":
|
|
212
|
+
sets = [self._smembers(k) for k in args]
|
|
213
|
+
return sets[0].difference(*sets[1:]) if sets else set()
|
|
214
|
+
|
|
215
|
+
# ── List commands ──────────────────────────────────────────────────
|
|
216
|
+
if cmd == "LPUSH":
|
|
217
|
+
return self._lpush(*args)
|
|
218
|
+
if cmd == "RPUSH":
|
|
219
|
+
return self._rpush(*args)
|
|
220
|
+
if cmd == "LRANGE":
|
|
221
|
+
return self._lrange(*args)
|
|
222
|
+
if cmd == "LLEN":
|
|
223
|
+
return self._llen(*args)
|
|
224
|
+
if cmd == "LINDEX":
|
|
225
|
+
return self._lindex(*args)
|
|
226
|
+
if cmd == "LSET":
|
|
227
|
+
return self._lset(*args)
|
|
228
|
+
if cmd == "LPOP":
|
|
229
|
+
return self._lpop(*args)
|
|
230
|
+
if cmd == "RPOP":
|
|
231
|
+
return self._rpop(*args)
|
|
232
|
+
|
|
233
|
+
raise RedisError(f"Unknown command: {command!r}")
|
|
234
|
+
|
|
235
|
+
# ── Internal helpers ──────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
def _put(self, coll: str, row_id: str, doc: Dict[str, Any], idem: Optional[str] = None) -> Any:
|
|
238
|
+
return self._db.put(coll, row_id, doc, client=self._client, idem=idem)
|
|
239
|
+
|
|
240
|
+
def _get_raw(self, coll: str, row_id: str) -> Optional[Dict[str, Any]]:
|
|
241
|
+
return self._db.get(coll, row_id)
|
|
242
|
+
|
|
243
|
+
def _del_raw(self, coll: str, row_id: str) -> None:
|
|
244
|
+
self._db.delete(coll, row_id, client=self._client)
|
|
245
|
+
|
|
246
|
+
# ── String ops ────────────────────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
def _set(self, key: str, value: Any, *opts: Any) -> str:
|
|
249
|
+
flags = {str(o).upper() for o in opts}
|
|
250
|
+
if "NX" in flags:
|
|
251
|
+
return self._setnx(key, value)
|
|
252
|
+
if "XX" in flags:
|
|
253
|
+
if self._get_raw(_KV_COLL, key) is None:
|
|
254
|
+
return None # type: ignore[return-value]
|
|
255
|
+
self._put(_KV_COLL, str(key), {"_v": str(value)})
|
|
256
|
+
return "OK"
|
|
257
|
+
|
|
258
|
+
def _get(self, key: str) -> Optional[str]:
|
|
259
|
+
doc = self._get_raw(_KV_COLL, str(key))
|
|
260
|
+
return str(doc["_v"]) if doc and "_v" in doc else None
|
|
261
|
+
|
|
262
|
+
def _getdel(self, key: str) -> Optional[str]:
|
|
263
|
+
val = self._get(key)
|
|
264
|
+
if val is not None:
|
|
265
|
+
self._del_raw(_KV_COLL, str(key))
|
|
266
|
+
return val
|
|
267
|
+
|
|
268
|
+
def _setnx(self, key: str, value: Any) -> int:
|
|
269
|
+
if self._get_raw(_KV_COLL, str(key)) is not None:
|
|
270
|
+
return 0
|
|
271
|
+
self._put(_KV_COLL, str(key), {"_v": str(value)})
|
|
272
|
+
return 1
|
|
273
|
+
|
|
274
|
+
def _mset(self, *pairs: Any) -> str:
|
|
275
|
+
if len(pairs) % 2:
|
|
276
|
+
raise RedisError("MSET requires an even number of arguments (key value ...)")
|
|
277
|
+
for i in range(0, len(pairs), 2):
|
|
278
|
+
self._set(pairs[i], pairs[i + 1])
|
|
279
|
+
return "OK"
|
|
280
|
+
|
|
281
|
+
def _mget(self, *keys: Any) -> List[Optional[str]]:
|
|
282
|
+
return [self._get(k) for k in keys]
|
|
283
|
+
|
|
284
|
+
def _del(self, *keys: Any) -> int:
|
|
285
|
+
count = 0
|
|
286
|
+
for k in keys:
|
|
287
|
+
doc = self._get_raw(_KV_COLL, str(k))
|
|
288
|
+
if doc is not None:
|
|
289
|
+
self._del_raw(_KV_COLL, str(k))
|
|
290
|
+
count += 1
|
|
291
|
+
return count
|
|
292
|
+
|
|
293
|
+
def _exists(self, *keys: Any) -> int:
|
|
294
|
+
return sum(1 for k in keys if self._get_raw(_KV_COLL, str(k)) is not None)
|
|
295
|
+
|
|
296
|
+
def _incrby(self, key: str, delta: int) -> int:
|
|
297
|
+
doc = self._get_raw(_KV_COLL, str(key))
|
|
298
|
+
current = int(doc["_v"]) if doc and "_v" in doc else 0
|
|
299
|
+
new_val = current + delta
|
|
300
|
+
self._put(_KV_COLL, str(key), {"_v": str(new_val)})
|
|
301
|
+
return new_val
|
|
302
|
+
|
|
303
|
+
def _append(self, key: str, value: str) -> int:
|
|
304
|
+
existing = self._get(key) or ""
|
|
305
|
+
combined = existing + str(value)
|
|
306
|
+
self._put(_KV_COLL, str(key), {"_v": combined})
|
|
307
|
+
return len(combined)
|
|
308
|
+
|
|
309
|
+
def _type(self, key: str) -> str:
|
|
310
|
+
if self._get_raw(_KV_COLL, str(key)) is not None:
|
|
311
|
+
return "string"
|
|
312
|
+
if self._hlen(str(key)) > 0:
|
|
313
|
+
return "hash"
|
|
314
|
+
if self._scard(str(key)) > 0:
|
|
315
|
+
return "set"
|
|
316
|
+
if self._llen(str(key)) > 0:
|
|
317
|
+
return "list"
|
|
318
|
+
return "none"
|
|
319
|
+
|
|
320
|
+
def _rename(self, src: str, dst: str) -> str:
|
|
321
|
+
val = self._get(src)
|
|
322
|
+
if val is None:
|
|
323
|
+
raise RedisError(f"ERR no such key: {src!r}")
|
|
324
|
+
self._set(dst, val)
|
|
325
|
+
self._del_raw(_KV_COLL, str(src))
|
|
326
|
+
return "OK"
|
|
327
|
+
|
|
328
|
+
def _keys(self, pattern: str = "*") -> List[str]:
|
|
329
|
+
import fnmatch
|
|
330
|
+
rows = self._db.query(f"FROM {_KV_COLL}")
|
|
331
|
+
all_keys = [r.get("_id", "") for r in rows]
|
|
332
|
+
if pattern == "*":
|
|
333
|
+
return all_keys
|
|
334
|
+
return [k for k in all_keys if fnmatch.fnmatch(k, pattern)]
|
|
335
|
+
|
|
336
|
+
def _flushdb(self) -> str:
|
|
337
|
+
for key in self._keys("*"):
|
|
338
|
+
self._del_raw(_KV_COLL, key)
|
|
339
|
+
return "OK"
|
|
340
|
+
|
|
341
|
+
# ── Hash ops (collection = hash name; id = field) ─────────────────────────
|
|
342
|
+
|
|
343
|
+
def _hset(self, name: str, *pairs: Any) -> int:
|
|
344
|
+
if len(pairs) % 2:
|
|
345
|
+
raise RedisError("HSET requires alternating field value pairs")
|
|
346
|
+
coll = _safe(str(name))
|
|
347
|
+
created = 0
|
|
348
|
+
for i in range(0, len(pairs), 2):
|
|
349
|
+
field, value = str(pairs[i]), pairs[i + 1]
|
|
350
|
+
existed = self._get_raw(coll, field) is not None
|
|
351
|
+
self._put(coll, field, {"_v": str(value)})
|
|
352
|
+
if not existed:
|
|
353
|
+
created += 1
|
|
354
|
+
return created
|
|
355
|
+
|
|
356
|
+
def _hsetnx(self, name: str, field: str, value: Any) -> int:
|
|
357
|
+
coll = _safe(str(name))
|
|
358
|
+
if self._get_raw(coll, str(field)) is not None:
|
|
359
|
+
return 0
|
|
360
|
+
self._put(coll, str(field), {"_v": str(value)})
|
|
361
|
+
return 1
|
|
362
|
+
|
|
363
|
+
def _hget(self, name: str, field: str) -> Optional[str]:
|
|
364
|
+
doc = self._get_raw(_safe(str(name)), str(field))
|
|
365
|
+
return str(doc["_v"]) if doc and "_v" in doc else None
|
|
366
|
+
|
|
367
|
+
def _hmget(self, name: str, *fields: Any) -> List[Optional[str]]:
|
|
368
|
+
return [self._hget(str(name), str(f)) for f in fields]
|
|
369
|
+
|
|
370
|
+
def _hgetall(self, name: str) -> Dict[str, str]:
|
|
371
|
+
rows = self._db.query(f"FROM {_safe(str(name))}")
|
|
372
|
+
return {r["_id"]: str(r["_v"]) for r in rows if "_v" in r}
|
|
373
|
+
|
|
374
|
+
def _hdel(self, name: str, *fields: Any) -> int:
|
|
375
|
+
coll = _safe(str(name))
|
|
376
|
+
count = 0
|
|
377
|
+
for f in fields:
|
|
378
|
+
if self._get_raw(coll, str(f)) is not None:
|
|
379
|
+
self._del_raw(coll, str(f))
|
|
380
|
+
count += 1
|
|
381
|
+
return count
|
|
382
|
+
|
|
383
|
+
def _hexists(self, name: str, field: str) -> int:
|
|
384
|
+
return 1 if self._get_raw(_safe(str(name)), str(field)) is not None else 0
|
|
385
|
+
|
|
386
|
+
def _hkeys(self, name: str) -> List[str]:
|
|
387
|
+
return [r["_id"] for r in self._db.query(f"FROM {_safe(str(name))}") if "_v" in r]
|
|
388
|
+
|
|
389
|
+
def _hvals(self, name: str) -> List[str]:
|
|
390
|
+
return [str(r["_v"]) for r in self._db.query(f"FROM {_safe(str(name))}") if "_v" in r]
|
|
391
|
+
|
|
392
|
+
def _hlen(self, name: str) -> int:
|
|
393
|
+
return len(self._hkeys(name))
|
|
394
|
+
|
|
395
|
+
def _hincrby(self, name: str, field: str, delta: Any) -> int:
|
|
396
|
+
coll = _safe(str(name))
|
|
397
|
+
doc = self._get_raw(coll, str(field))
|
|
398
|
+
current = int(doc["_v"]) if doc and "_v" in doc else 0
|
|
399
|
+
new_val = current + int(delta)
|
|
400
|
+
self._put(coll, str(field), {"_v": str(new_val)})
|
|
401
|
+
return new_val
|
|
402
|
+
|
|
403
|
+
# ── Set ops (using NEDB relations) ────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
def _set_node(self, key: str) -> str:
|
|
406
|
+
return f"{_SET_NODE_PREFIX}{_safe(key)}"
|
|
407
|
+
|
|
408
|
+
def _set_member_node(self, key: str, member: str) -> str:
|
|
409
|
+
return f"{_SET_MEMBER_PREFIX}{_safe(key)}__{_safe(member)}"
|
|
410
|
+
|
|
411
|
+
def _sadd(self, key: str, *members: Any) -> int:
|
|
412
|
+
count = 0
|
|
413
|
+
node = self._set_node(str(key))
|
|
414
|
+
existing = set(self._smembers(str(key)))
|
|
415
|
+
for m in members:
|
|
416
|
+
ms = str(m)
|
|
417
|
+
if ms not in existing:
|
|
418
|
+
target = self._set_member_node(str(key), ms)
|
|
419
|
+
self._db.link(node, "member", target, client=self._client)
|
|
420
|
+
existing.add(ms)
|
|
421
|
+
count += 1
|
|
422
|
+
return count
|
|
423
|
+
|
|
424
|
+
def _smembers(self, key: str) -> Set[str]:
|
|
425
|
+
node = self._set_node(str(key))
|
|
426
|
+
prefix = f"{_SET_MEMBER_PREFIX}{_safe(str(key))}__"
|
|
427
|
+
neighbors = self._db.neighbors(node, "member")
|
|
428
|
+
return {nb[len(prefix):] for nb in neighbors if nb.startswith(prefix)}
|
|
429
|
+
|
|
430
|
+
def _sismember(self, key: str, member: str) -> int:
|
|
431
|
+
return 1 if str(member) in self._smembers(str(key)) else 0
|
|
432
|
+
|
|
433
|
+
def _srem(self, key: str, *members: Any) -> int:
|
|
434
|
+
count = 0
|
|
435
|
+
node = self._set_node(str(key))
|
|
436
|
+
existing = self._smembers(str(key))
|
|
437
|
+
for m in members:
|
|
438
|
+
ms = str(m)
|
|
439
|
+
if ms in existing:
|
|
440
|
+
target = self._set_member_node(str(key), ms)
|
|
441
|
+
self._db.unlink(node, "member", target, client=self._client)
|
|
442
|
+
count += 1
|
|
443
|
+
return count
|
|
444
|
+
|
|
445
|
+
def _scard(self, key: str) -> int:
|
|
446
|
+
return len(self._smembers(str(key)))
|
|
447
|
+
|
|
448
|
+
# ── List ops (append-only; id = zero-padded timestamp for order) ──────────
|
|
449
|
+
|
|
450
|
+
def _list_coll(self, key: str) -> str:
|
|
451
|
+
return f"{_LIST_PREFIX}{_safe(key)}"
|
|
452
|
+
|
|
453
|
+
def _lpush(self, key: str, *values: Any) -> int:
|
|
454
|
+
coll = self._list_coll(str(key))
|
|
455
|
+
for v in reversed(values):
|
|
456
|
+
seq_id = f"L-{time.monotonic_ns():020d}-{uuid.uuid4().hex[:6]}"
|
|
457
|
+
self._put(coll, seq_id, {"_v": str(v), "_side": "L", "_ts": time.monotonic_ns()})
|
|
458
|
+
return self._llen(str(key))
|
|
459
|
+
|
|
460
|
+
def _rpush(self, key: str, *values: Any) -> int:
|
|
461
|
+
coll = self._list_coll(str(key))
|
|
462
|
+
for v in values:
|
|
463
|
+
seq_id = f"R-{time.monotonic_ns():020d}-{uuid.uuid4().hex[:6]}"
|
|
464
|
+
self._put(coll, seq_id, {"_v": str(v), "_side": "R", "_ts": time.monotonic_ns()})
|
|
465
|
+
return self._llen(str(key))
|
|
466
|
+
|
|
467
|
+
def _list_rows(self, key: str) -> List[Dict[str, Any]]:
|
|
468
|
+
coll = self._list_coll(str(key))
|
|
469
|
+
rows = self._db.query(f"FROM {coll}")
|
|
470
|
+
return sorted(rows, key=lambda r: (r.get("_side", "R"), r.get("_ts", 0)))
|
|
471
|
+
|
|
472
|
+
def _lrange(self, key: str, start: Any, stop: Any) -> List[str]:
|
|
473
|
+
rows = self._list_rows(str(key))
|
|
474
|
+
vals = [str(r["_v"]) for r in rows if "_v" in r]
|
|
475
|
+
start, stop = int(start), int(stop)
|
|
476
|
+
if stop == -1:
|
|
477
|
+
stop = len(vals) - 1
|
|
478
|
+
return vals[start:stop + 1]
|
|
479
|
+
|
|
480
|
+
def _llen(self, key: str) -> int:
|
|
481
|
+
return len(self._list_rows(str(key)))
|
|
482
|
+
|
|
483
|
+
def _lindex(self, key: str, index: Any) -> Optional[str]:
|
|
484
|
+
vals = self._lrange(str(key), 0, -1)
|
|
485
|
+
i = int(index)
|
|
486
|
+
return vals[i] if 0 <= i < len(vals) else (vals[i] if -len(vals) <= i < 0 else None)
|
|
487
|
+
|
|
488
|
+
def _lset(self, key: str, index: Any, value: Any) -> str:
|
|
489
|
+
rows = self._list_rows(str(key))
|
|
490
|
+
i = int(index)
|
|
491
|
+
if i < 0:
|
|
492
|
+
i = len(rows) + i
|
|
493
|
+
if i < 0 or i >= len(rows):
|
|
494
|
+
raise RedisError("ERR index out of range")
|
|
495
|
+
coll = self._list_coll(str(key))
|
|
496
|
+
row = rows[i]
|
|
497
|
+
self._put(coll, row["_id"], {**row, "_v": str(value)})
|
|
498
|
+
return "OK"
|
|
499
|
+
|
|
500
|
+
def _lpop(self, key: str) -> Optional[str]:
|
|
501
|
+
rows = self._list_rows(str(key))
|
|
502
|
+
if not rows:
|
|
503
|
+
return None
|
|
504
|
+
r = rows[0]
|
|
505
|
+
coll = self._list_coll(str(key))
|
|
506
|
+
self._del_raw(coll, r["_id"])
|
|
507
|
+
return str(r.get("_v", ""))
|
|
508
|
+
|
|
509
|
+
def _rpop(self, key: str) -> Optional[str]:
|
|
510
|
+
rows = self._list_rows(str(key))
|
|
511
|
+
if not rows:
|
|
512
|
+
return None
|
|
513
|
+
r = rows[-1]
|
|
514
|
+
coll = self._list_coll(str(key))
|
|
515
|
+
self._del_raw(coll, r["_id"])
|
|
516
|
+
return str(r.get("_v", ""))
|
nedb/relations.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
nedb.relations — first-class, time-travel-aware relations (the graph layer).
|
|
3
|
+
|
|
4
|
+
Relations are stored as adjacency lists for O(1) traversal. Each edge records the
|
|
5
|
+
seq at which it was added and (optionally) removed, so relation queries can also be
|
|
6
|
+
asked "AS OF" any past sequence — the graph time-travels just like the records do.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Relations:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
# (frm, rel) -> list of [to, added_seq, removed_seq|None]
|
|
16
|
+
self._adj: Dict[Tuple[str, str], List[list]] = {}
|
|
17
|
+
# (to, rel) -> list of [frm, added_seq, removed_seq|None] (reverse index)
|
|
18
|
+
self._radj: Dict[Tuple[str, str], List[list]] = {}
|
|
19
|
+
|
|
20
|
+
def link(self, frm: str, rel: str, to: str, seq: int) -> None:
|
|
21
|
+
for e in self._adj.get((frm, rel), []):
|
|
22
|
+
if e[0] == to and e[2] is None:
|
|
23
|
+
return # already linked
|
|
24
|
+
self._adj.setdefault((frm, rel), []).append([to, seq, None])
|
|
25
|
+
self._radj.setdefault((to, rel), []).append([frm, seq, None])
|
|
26
|
+
|
|
27
|
+
def unlink(self, frm: str, rel: str, to: str, seq: int) -> None:
|
|
28
|
+
for e in self._adj.get((frm, rel), []):
|
|
29
|
+
if e[0] == to and e[2] is None:
|
|
30
|
+
e[2] = seq
|
|
31
|
+
for e in self._radj.get((to, rel), []):
|
|
32
|
+
if e[0] == frm and e[2] is None:
|
|
33
|
+
e[2] = seq
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _live(edges, as_of):
|
|
37
|
+
out = []
|
|
38
|
+
for node, added, removed in edges:
|
|
39
|
+
if as_of is None:
|
|
40
|
+
if removed is None:
|
|
41
|
+
out.append(node)
|
|
42
|
+
else:
|
|
43
|
+
if added <= as_of and (removed is None or removed > as_of):
|
|
44
|
+
out.append(node)
|
|
45
|
+
return out
|
|
46
|
+
|
|
47
|
+
def neighbors(self, frm: str, rel: str, as_of: Optional[int] = None) -> List[str]:
|
|
48
|
+
return self._live(self._adj.get((frm, rel), []), as_of)
|
|
49
|
+
|
|
50
|
+
def inbound(self, to: str, rel: str, as_of: Optional[int] = None) -> List[str]:
|
|
51
|
+
return self._live(self._radj.get((to, rel), []), as_of)
|