nedb-engine 2.6.0__tar.gz → 2.6.2__tar.gz
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.
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/PKG-INFO +1 -1
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/pyproject.toml +1 -1
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/__init__.py +3 -3
- nedb_engine-2.6.2/python/nedb/_native.abi3.so +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/concurrent.py +9 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/engine.py +102 -0
- nedb_engine-2.6.2/python/nedb/nedbd-v2 +0 -0
- nedb_engine-2.6.2/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- nedb_engine-2.6.2/python/nedb/nedbd-v2-darwin-x64 +0 -0
- nedb_engine-2.6.2/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/server.py +37 -19
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/store.py +10 -0
- nedb_engine-2.6.0/python/nedb/_native.abi3.so +0 -0
- nedb_engine-2.6.0/python/nedb/nedbd-v2 +0 -0
- nedb_engine-2.6.0/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- nedb_engine-2.6.0/python/nedb/nedbd-v2-darwin-x64 +0 -0
- nedb_engine-2.6.0/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/.gitignore +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/LICENSE +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/README.md +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/client/node/README.md +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/client/python/README.md +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/examples/mini-chain/README.md +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/autoindex.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/cascade.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/crypto.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/index.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/log.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/merkle.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/mongo.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/proof.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/query.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/relations.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/resp2.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/snapshot.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/sql.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/python/nedb/wrap_redis.py +0 -0
- {nedb_engine-2.6.0 → nedb_engine-2.6.2}/rust/nedb-v2/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 2.6.
|
|
3
|
+
Version: 2.6.2
|
|
4
4
|
Summary: NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd).
|
|
5
5
|
Project-URL: Homepage, https://github.com/aiassistsecure/nedb
|
|
6
6
|
Project-URL: Repository, https://github.com/aiassistsecure/nedb
|
|
@@ -10,7 +10,7 @@ build-backend = "hatchling.build"
|
|
|
10
10
|
|
|
11
11
|
[project]
|
|
12
12
|
name = "nedb-engine"
|
|
13
|
-
version = "2.6.
|
|
13
|
+
version = "2.6.2"
|
|
14
14
|
description = "NEDB — a versioned, self-compressing, time-traveling embedded database (replay-protected, idempotent, relational, searchable) with durable AOF persistence and a server daemon (nedbd)."
|
|
15
15
|
readme = "README.md"
|
|
16
16
|
requires-python = ">=3.8"
|
|
@@ -16,7 +16,7 @@ as ``nedb._native`` (``nedb.__has_native__`` reports whether it loaded).
|
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
from .engine import NEDB
|
|
19
|
+
from .engine import NEDB, PreconditionFailed
|
|
20
20
|
from .log import Op, OpLog, ReplayError
|
|
21
21
|
from .query import Query, parse_nql
|
|
22
22
|
from .snapshot import save_snapshot, load_snapshot
|
|
@@ -79,7 +79,7 @@ except ImportError: # pure-Python install (sdist / unsupported platform)
|
|
|
79
79
|
del _types, _sys, _NativeStub, _native_stub
|
|
80
80
|
|
|
81
81
|
__all__ = [
|
|
82
|
-
"NEDB", "OpLog", "Op", "ReplayError", "Query", "parse_nql",
|
|
82
|
+
"NEDB", "PreconditionFailed", "OpLog", "Op", "ReplayError", "Query", "parse_nql",
|
|
83
83
|
"save_snapshot", "load_snapshot",
|
|
84
84
|
"sql_exec", "sql_to_nql", "SQLError", "SQLUnsupportedError",
|
|
85
85
|
"RedisCompat", "RedisError", "RedisUnsupportedError",
|
|
@@ -89,4 +89,4 @@ __all__ = [
|
|
|
89
89
|
"verify_proof", "fold_head",
|
|
90
90
|
"_native", "__has_native__",
|
|
91
91
|
]
|
|
92
|
-
__version__ = "2.6.
|
|
92
|
+
__version__ = "2.6.2"
|
|
Binary file
|
|
@@ -90,6 +90,13 @@ class Sequencer:
|
|
|
90
90
|
def put(self, coll: str, id: str, doc: dict, **kw: Any) -> Any:
|
|
91
91
|
return self._submit("put", coll, id, doc, **kw)
|
|
92
92
|
|
|
93
|
+
def tx(self, ops: list, **kw: Any) -> Any:
|
|
94
|
+
"""Atomic multi-op transaction (see NEDB.tx). Submitted as ONE
|
|
95
|
+
intent, so the committer validates preconditions and applies all ops
|
|
96
|
+
with nothing interleaving — the single-writer thread IS the
|
|
97
|
+
serialization point, exactly as Redis's single thread was for Lua."""
|
|
98
|
+
return self._submit("tx", ops, **kw)
|
|
99
|
+
|
|
93
100
|
def delete(self, coll: str, id: str, **kw: Any) -> Any:
|
|
94
101
|
return self._submit("delete", coll, id, **kw)
|
|
95
102
|
|
|
@@ -207,6 +214,8 @@ class Sequencer:
|
|
|
207
214
|
return db.put_file(*intent.args, **intent.kwargs)
|
|
208
215
|
if k == "checkpoint":
|
|
209
216
|
return db.checkpoint()
|
|
217
|
+
if k == "tx":
|
|
218
|
+
return db.tx(*intent.args, **intent.kwargs)
|
|
210
219
|
raise ValueError(f"unknown write kind: {k}")
|
|
211
220
|
|
|
212
221
|
def close(self) -> None:
|
|
@@ -24,6 +24,23 @@ from .relations import Relations
|
|
|
24
24
|
from .store import MVCCStore
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class PreconditionFailed(Exception):
|
|
28
|
+
"""An `if_seq` compare-and-set precondition did not hold; the transaction
|
|
29
|
+
was rejected as a unit and NOTHING was applied. `.failures` lists every
|
|
30
|
+
failed check as {index, coll, id, expected, actual}."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, failures: List[dict]):
|
|
33
|
+
self.failures = failures
|
|
34
|
+
super().__init__(
|
|
35
|
+
f"{len(failures)} precondition(s) failed: "
|
|
36
|
+
+ "; ".join(
|
|
37
|
+
f"[{f['index']}] {f['coll']}:{f['id']} expected seq "
|
|
38
|
+
f"{f['expected']}, actual {f['actual']}"
|
|
39
|
+
for f in failures
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
27
44
|
def apply_op(store: MVCCStore, relations: Relations, indexes: Indexes, op: Op,
|
|
28
45
|
cause_map: Optional[Dict[int, List[int]]] = None) -> None:
|
|
29
46
|
"""Deterministically fold one op into materialized state."""
|
|
@@ -484,6 +501,91 @@ class NEDB:
|
|
|
484
501
|
return self._check_ttl(coll, id, doc)
|
|
485
502
|
return doc # time-travel reads never trigger lazy expiry
|
|
486
503
|
|
|
504
|
+
def last_seq(self, coll: str, id: str) -> Optional[int]:
|
|
505
|
+
"""Version anchor for compare-and-set: the seq of the most recent op
|
|
506
|
+
(put OR delete) that touched this document, or None if never written."""
|
|
507
|
+
return self.store.last_seq(f"{coll}:{id}")
|
|
508
|
+
|
|
509
|
+
def tx(self, ops: List[dict], default_client: str = "local") -> dict:
|
|
510
|
+
"""Atomic multi-op transaction with optional per-op CAS preconditions.
|
|
511
|
+
|
|
512
|
+
Each op is a dict:
|
|
513
|
+
{"op": "put", "coll", "id", "doc", "if_seq"?, "idem"?, "client"?,
|
|
514
|
+
"caused_by"?, "ttl_s"?}
|
|
515
|
+
{"op": "del", "coll", "id", "if_seq"?, "idem"?, "client"?}
|
|
516
|
+
{"op": "link", "frm", "rel", "to", "client"?}
|
|
517
|
+
|
|
518
|
+
`if_seq` semantics (the Lua-atomics replacement contract):
|
|
519
|
+
if_seq = N -> the doc's last_seq must be exactly N (the caller
|
|
520
|
+
read version N and nothing has touched it since)
|
|
521
|
+
if_seq = -1 -> the doc must NOT currently exist (create-once)
|
|
522
|
+
omitted/None -> unconditional
|
|
523
|
+
|
|
524
|
+
Two phases: ALL preconditions are validated against current state
|
|
525
|
+
first; if any fails, PreconditionFailed is raised and NOTHING is
|
|
526
|
+
applied. Only then are the ops applied in order (each becoming a
|
|
527
|
+
normal chained, hash-sealed op-log entry).
|
|
528
|
+
|
|
529
|
+
Concurrency contract: this method performs no locking of its own.
|
|
530
|
+
It is atomic when executed by a single writer — which is exactly how
|
|
531
|
+
the daemon runs it (one intent on the Sequencer's committer thread,
|
|
532
|
+
the same single-writer property that made Redis Lua scripts atomic).
|
|
533
|
+
Embedded multi-threaded callers must route through Sequencer.tx().
|
|
534
|
+
|
|
535
|
+
Durability: the Sequencer group-commits, so a tx submitted as one
|
|
536
|
+
intent shares a single fsync boundary — a crash before it loses the
|
|
537
|
+
whole tx together, never a prefix of it.
|
|
538
|
+
|
|
539
|
+
Precondition reads use the raw store (no TTL side effects): an
|
|
540
|
+
expired-but-unswept doc still counts as existing for if_seq == -1;
|
|
541
|
+
call sweep() first where create-once interacts with TTLs.
|
|
542
|
+
"""
|
|
543
|
+
failures: List[dict] = []
|
|
544
|
+
for i, op in enumerate(ops):
|
|
545
|
+
want = op.get("if_seq")
|
|
546
|
+
if want is None:
|
|
547
|
+
continue
|
|
548
|
+
kind = str(op.get("op", "put")).lower()
|
|
549
|
+
if kind not in ("put", "del", "delete"):
|
|
550
|
+
continue # links carry no per-doc version
|
|
551
|
+
coll, rid = str(op["coll"]), str(op["id"])
|
|
552
|
+
cur = self.store.last_seq(f"{coll}:{rid}")
|
|
553
|
+
want = int(want)
|
|
554
|
+
if want == -1:
|
|
555
|
+
if self.store.get(f"{coll}:{rid}") is not None:
|
|
556
|
+
failures.append({"index": i, "coll": coll, "id": rid,
|
|
557
|
+
"expected": -1, "actual": cur})
|
|
558
|
+
elif cur != want:
|
|
559
|
+
failures.append({"index": i, "coll": coll, "id": rid,
|
|
560
|
+
"expected": want, "actual": cur})
|
|
561
|
+
if failures:
|
|
562
|
+
raise PreconditionFailed(failures)
|
|
563
|
+
|
|
564
|
+
results: List[dict] = []
|
|
565
|
+
for op in ops:
|
|
566
|
+
kind = str(op.get("op", "put")).lower()
|
|
567
|
+
client = str(op.get("client") or default_client)
|
|
568
|
+
if kind == "put":
|
|
569
|
+
stored = self.put(str(op["coll"]), str(op["id"]),
|
|
570
|
+
dict(op.get("doc") or {}),
|
|
571
|
+
client=client,
|
|
572
|
+
idem=op.get("idem"),
|
|
573
|
+
ttl_s=op.get("ttl_s"),
|
|
574
|
+
caused_by=op.get("caused_by"))
|
|
575
|
+
results.append({"op": "put", "id": op["id"], "seq": self.seq,
|
|
576
|
+
"doc": stored})
|
|
577
|
+
elif kind in ("del", "delete"):
|
|
578
|
+
self.delete(str(op["coll"]), str(op["id"]),
|
|
579
|
+
client=client, idem=op.get("idem"))
|
|
580
|
+
results.append({"op": "del", "id": op["id"], "seq": self.seq})
|
|
581
|
+
elif kind == "link":
|
|
582
|
+
self.link(str(op["frm"]), str(op["rel"]), str(op["to"]),
|
|
583
|
+
client=client)
|
|
584
|
+
results.append({"op": "link", "seq": self.seq})
|
|
585
|
+
else:
|
|
586
|
+
results.append({"op": kind, "error": "unknown op"})
|
|
587
|
+
return {"results": results, "count": len(results), "seq": self.seq}
|
|
588
|
+
|
|
487
589
|
def expire(self, coll: str, id: str, ttl_s: float) -> bool:
|
|
488
590
|
"""Set or update the TTL on an existing document. Returns False if not found."""
|
|
489
591
|
doc = self.store.get(f"{coll}:{id}")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -35,7 +35,11 @@ HTTP API (all JSON):
|
|
|
35
35
|
GET /v1/databases/<name>/files/<filename>/root?version=N&tier=warm — Merkle root (anchorable)
|
|
36
36
|
POST /v1/databases/<name>/proof {hash} — Merkle inclusion proof (verifiable offline)
|
|
37
37
|
POST /v1/databases/<name>/checkpoint — on-demand checkpoint
|
|
38
|
-
|
|
38
|
+
POST /v1/databases/<name>/batch — ATOMIC multi-op tx: {ops: [{op, coll, id,
|
|
39
|
+
doc?, if_seq?, idem?, client?}], client?}
|
|
40
|
+
all-or-nothing; if_seq CAS preconditions
|
|
41
|
+
(N = exact version, -1 = must-not-exist);
|
|
42
|
+
409 {error: "precondition_failed", failures}
|
|
39
43
|
"""
|
|
40
44
|
from __future__ import annotations
|
|
41
45
|
|
|
@@ -52,6 +56,7 @@ from . import __version__
|
|
|
52
56
|
from .engine import NEDB
|
|
53
57
|
from .concurrent import Sequencer
|
|
54
58
|
from .log import ReplayError
|
|
59
|
+
from .engine import PreconditionFailed
|
|
55
60
|
|
|
56
61
|
NAME_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}")
|
|
57
62
|
|
|
@@ -525,30 +530,43 @@ def make_handler(manager: Manager, token: Optional[str]):
|
|
|
525
530
|
"size": len(data), "tier": tier})
|
|
526
531
|
return
|
|
527
532
|
|
|
528
|
-
# POST /v1/databases/<name>/batch —
|
|
529
|
-
# Body: {ops: [{op:"put"|"del"|"link", coll, id, doc?, frm?, rel?,
|
|
533
|
+
# POST /v1/databases/<name>/batch — ATOMIC multi-op transaction.
|
|
534
|
+
# Body: {ops: [{op:"put"|"del"|"link", coll, id, doc?, frm?, rel?,
|
|
535
|
+
# to?, if_seq?, idem?, client?, caused_by?, ttl_s?}, ...],
|
|
536
|
+
# client?: default client for ops that don't set one}
|
|
537
|
+
#
|
|
538
|
+
# All-or-nothing: every `if_seq` precondition is validated against
|
|
539
|
+
# current state FIRST (under the Sequencer's single committer
|
|
540
|
+
# thread — nothing can interleave); if any fails, HTTP 409 with
|
|
541
|
+
# the structured failure list and NOTHING is applied.
|
|
542
|
+
#
|
|
543
|
+
# `if_seq` contract (the Lua-atomics replacement):
|
|
544
|
+
# if_seq = N doc's last_seq must be exactly N (CAS)
|
|
545
|
+
# if_seq = -1 doc must not exist (create-once)
|
|
546
|
+
# omitted unconditional
|
|
547
|
+
#
|
|
548
|
+
# Per-op `idem` and `client` are forwarded to the op log (idem
|
|
549
|
+
# keys dedupe engine-side; client attributes provenance).
|
|
530
550
|
if method == "POST" and len(parts) == 4 and parts[:2] == ["v1", "databases"] and parts[3] == "batch":
|
|
531
551
|
db = manager.require(parts[2])
|
|
532
552
|
b = self._body()
|
|
533
553
|
ops_list = b.get("ops") or []
|
|
534
554
|
if not isinstance(ops_list, list) or not ops_list:
|
|
535
555
|
raise HttpError(400, "ops array is required")
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
self._send(200, {"results": results, "count": len(results),
|
|
551
|
-
"seq": db.seq, "head": db.head})
|
|
556
|
+
default_client = str(b.get("client") or "http")
|
|
557
|
+
try:
|
|
558
|
+
out = db.tx(ops_list, default_client=default_client)
|
|
559
|
+
except PreconditionFailed as e:
|
|
560
|
+
self._send(409, {"error": "precondition_failed",
|
|
561
|
+
"failures": e.failures,
|
|
562
|
+
"seq": db.seq})
|
|
563
|
+
return
|
|
564
|
+
except ReplayError as e:
|
|
565
|
+
raise HttpError(409, str(e))
|
|
566
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
567
|
+
raise HttpError(400, f"bad op: {e}")
|
|
568
|
+
self._send(200, {"results": out["results"], "count": out["count"],
|
|
569
|
+
"atomic": True, "seq": db.seq, "head": db.head})
|
|
552
570
|
return
|
|
553
571
|
|
|
554
572
|
raise HttpError(404, "no such route")
|
|
@@ -40,6 +40,16 @@ class MVCCStore:
|
|
|
40
40
|
_, val = chain[i]
|
|
41
41
|
return None if val is TOMB else val
|
|
42
42
|
|
|
43
|
+
def last_seq(self, key: str) -> Optional[int]:
|
|
44
|
+
"""Seq of the most recent op that touched this key (including a
|
|
45
|
+
delete's tombstone), or None if the key has never been written.
|
|
46
|
+
|
|
47
|
+
This is the version anchor for compare-and-set (`if_seq`): a CAS
|
|
48
|
+
taken against version N correctly fails after ANY later write or
|
|
49
|
+
delete, because that op advanced the key's last_seq past N."""
|
|
50
|
+
seqs = self._seqs.get(key)
|
|
51
|
+
return seqs[-1] if seqs else None
|
|
52
|
+
|
|
43
53
|
def keys(self, prefix: str = "", as_of: Optional[int] = None) -> List[str]:
|
|
44
54
|
# Lock-free read: take an atomic snapshot of the key set, then resolve
|
|
45
55
|
# versions by `as_of`. Under the concurrent Sequencer a single committer
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|