nedb-engine 3.2.1__tar.gz → 3.2.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-3.2.1 → nedb_engine-3.2.2}/PKG-INFO +1 -1
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/pyproject.toml +1 -1
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/__init__.py +1 -1
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/_native.abi3.so +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/dag.py +10 -3
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2 +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2-darwin-arm64 +0 -0
- nedb_engine-3.2.2/python/nedb/nedbd-v2-darwin-x64 +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2.exe +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_core.py +65 -5
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_redis.py +42 -3
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_sqlite.py +132 -31
- nedb_engine-3.2.1/python/nedb/nedbd-v2-darwin-x64 +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/.gitignore +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/LICENSE +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/client/node/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/client/python/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/examples/agent-loop/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/examples/mini-chain/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/autoindex.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/__init__.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/redis_backend.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/cascade.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/client.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/concurrent.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/crypto.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/engine.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/index.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/log.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/merkle.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/mongo.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/proof.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/query.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/redis_compat.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/relations.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/resp2.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/server.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/snapshot.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/sql.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/store.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_mongo.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_mysql.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_postgresql.py +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/rust/crates/nedb-wrap/README.md +0 -0
- {nedb_engine-3.2.1 → nedb_engine-3.2.2}/rust/nedb-v2/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: nedb-engine
|
|
3
|
-
Version: 3.2.
|
|
3
|
+
Version: 3.2.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 = "3.2.
|
|
13
|
+
version = "3.2.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"
|
|
Binary file
|
|
@@ -32,10 +32,17 @@ from typing import Any, Dict, List, Optional
|
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def has_dag_native() -> bool:
|
|
35
|
-
"""True when the embedded Rust v2/v3 DAG core is
|
|
35
|
+
"""True when the embedded Rust v2/v3 DAG core is actually LOADED.
|
|
36
|
+
|
|
37
|
+
This used to be `from .. import __has_native__; return True` — which tests
|
|
38
|
+
whether the NAME imports, never what it holds. `__has_native__` is always
|
|
39
|
+
defined (it is False on the universal pure-Python wheel), so this returned
|
|
40
|
+
True unconditionally and `backend="auto"` chose the DAG on machines that
|
|
41
|
+
had no compiled core at all.
|
|
42
|
+
"""
|
|
36
43
|
try:
|
|
37
|
-
from .. import __has_native__
|
|
38
|
-
return
|
|
44
|
+
from .. import __has_native__
|
|
45
|
+
return bool(__has_native__)
|
|
39
46
|
except Exception:
|
|
40
47
|
return False
|
|
41
48
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -64,8 +64,16 @@ def open_engine(
|
|
|
64
64
|
return NedBdProxy(nedbd_url, db_name, token=nedbd_token), None
|
|
65
65
|
|
|
66
66
|
if backend == "dag" or (backend == "auto" and has_dag_native()):
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
try:
|
|
68
|
+
return _open_dag(dag_path, dag_tmk), None
|
|
69
|
+
except ImportError:
|
|
70
|
+
if backend == "dag":
|
|
71
|
+
raise # an explicit request must never silently downgrade
|
|
72
|
+
# auto: no compiled core after all — fall through to v1 AOF, the
|
|
73
|
+
# universal fallback. wrap_redis has always done this; open_engine
|
|
74
|
+
# did not, so wrap_sqlite / wrap_mysql / wrap_mongo /
|
|
75
|
+
# wrap_postgresql raised ImportError out of the constructor on
|
|
76
|
+
# every install without a platform wheel.
|
|
69
77
|
|
|
70
78
|
if backend in ("aof", "auto"):
|
|
71
79
|
return _NEDB(), None
|
|
@@ -74,6 +82,24 @@ def open_engine(
|
|
|
74
82
|
"(expected 'auto' | 'aof' | 'dag' | 'nedbd')")
|
|
75
83
|
|
|
76
84
|
|
|
85
|
+
def _open_dag(dag_path, dag_tmk):
|
|
86
|
+
"""Construct the embedded DAG backend, warning when it is not durable."""
|
|
87
|
+
if dag_path is None:
|
|
88
|
+
# An embedded DAG with no path is IN-MEMORY: the chain dies with the
|
|
89
|
+
# process. These adapters have no durable in-host fallback the way
|
|
90
|
+
# wrap_redis has Redis Streams, so the choice stands -- but silently
|
|
91
|
+
# handing someone a provenance log that evaporates is exactly the
|
|
92
|
+
# failure this engine exists to prevent. Say so, once.
|
|
93
|
+
import warnings
|
|
94
|
+
warnings.warn(
|
|
95
|
+
"NEDB: embedded DAG opened with no dag_path= — this chain is "
|
|
96
|
+
"IN-MEMORY and will not survive the process. Pass "
|
|
97
|
+
'dag_path="./audit" for a durable store, or nedbd_url= to use '
|
|
98
|
+
"a server.",
|
|
99
|
+
RuntimeWarning, stacklevel=4)
|
|
100
|
+
return DagBackend(path=dag_path, tmk=dag_tmk)
|
|
101
|
+
|
|
102
|
+
|
|
77
103
|
# ── The shared surface ───────────────────────────────────────────────────────
|
|
78
104
|
|
|
79
105
|
class WrapSurface:
|
|
@@ -91,6 +117,36 @@ class WrapSurface:
|
|
|
91
117
|
self._backfilled: bool = False
|
|
92
118
|
self._db = engine
|
|
93
119
|
self._persist = persist or _MemoryPersist()
|
|
120
|
+
# ── shadow observability ────────────────────────────────────────────
|
|
121
|
+
# A shadow failure must never break the host database call, but
|
|
122
|
+
# "don't raise into the host's stack" and "don't tell anyone" are
|
|
123
|
+
# different requirements. For a PROVENANCE layer, silently dropping a
|
|
124
|
+
# record is the worst available outcome: the chain gets a hole and
|
|
125
|
+
# verify() still returns True, because it verifies what got in, not
|
|
126
|
+
# that everything that should have got in did.
|
|
127
|
+
#
|
|
128
|
+
# Every swallowed failure is now counted and, optionally, reported.
|
|
129
|
+
self.shadow_errors: int = 0
|
|
130
|
+
self.last_shadow_error: Optional[str] = None
|
|
131
|
+
self.on_shadow_error: Optional[Callable[[BaseException], None]] = None
|
|
132
|
+
# True when the chain outlives the process — set by the adapter.
|
|
133
|
+
self.durable: bool = True
|
|
134
|
+
# strict=True re-raises instead of swallowing — for tests and for
|
|
135
|
+
# deployments that would rather fail loudly than lose provenance.
|
|
136
|
+
self.strict_shadow: bool = False
|
|
137
|
+
|
|
138
|
+
def note_shadow_error(self, exc: BaseException) -> None:
|
|
139
|
+
"""Record a swallowed shadow failure. Never raises unless strict."""
|
|
140
|
+
self.shadow_errors += 1
|
|
141
|
+
self.last_shadow_error = f"{type(exc).__name__}: {exc}"
|
|
142
|
+
cb = self.on_shadow_error
|
|
143
|
+
if cb is not None:
|
|
144
|
+
try:
|
|
145
|
+
cb(exc)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass # a broken callback must not compound the original failure
|
|
148
|
+
if self.strict_shadow:
|
|
149
|
+
raise exc
|
|
94
150
|
|
|
95
151
|
# ── host hooks (override in the host adapter) ────────────────────────────
|
|
96
152
|
|
|
@@ -139,7 +195,11 @@ class WrapSurface:
|
|
|
139
195
|
try:
|
|
140
196
|
self._db.put(m.collection, doc_id, doc, client="__backfill__")
|
|
141
197
|
total += 1
|
|
142
|
-
except Exception:
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Counted, not silent: backfill() returns the number of rows
|
|
200
|
+
# imported, and a caller comparing that to the host's row
|
|
201
|
+
# count deserves to know the difference was errors.
|
|
202
|
+
self.note_shadow_error(e)
|
|
143
203
|
continue
|
|
144
204
|
self._backfilled = True
|
|
145
205
|
return total
|
|
@@ -165,8 +225,8 @@ class WrapSurface:
|
|
|
165
225
|
self._db.put(m.collection, m.extract_id(key), doc,
|
|
166
226
|
client="__shadow__")
|
|
167
227
|
self._persist_after()
|
|
168
|
-
except Exception:
|
|
169
|
-
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self.note_shadow_error(e)
|
|
170
230
|
|
|
171
231
|
def _persist_after(self) -> None:
|
|
172
232
|
# DAG + nedbd persist themselves; v1 AOF needs the last op appended
|
|
@@ -302,6 +302,13 @@ class NEDBSurface:
|
|
|
302
302
|
self._mappings: List[CollectionMapping] = []
|
|
303
303
|
self.shadow_writes: bool = False
|
|
304
304
|
self._backfilled: bool = False
|
|
305
|
+
self.shadow_errors: int = 0
|
|
306
|
+
self.last_shadow_error: Optional[str] = None
|
|
307
|
+
self.on_shadow_error = None
|
|
308
|
+
self.strict_shadow: bool = False
|
|
309
|
+
# True when the chain outlives the process. Introspectable rather than
|
|
310
|
+
# inferred: durability should never be something you have to guess.
|
|
311
|
+
self.durable: bool = True
|
|
305
312
|
# Set BEFORE any branching. _reload() reads this, and the
|
|
306
313
|
# DagBackend-ImportError fallback below reaches _reload() without
|
|
307
314
|
# passing through a branch that assigns it — which made wrap_redis()
|
|
@@ -315,13 +322,26 @@ class NEDBSurface:
|
|
|
315
322
|
self._nedbd_mode = True
|
|
316
323
|
self._db = NedBdProxy(nedbd_url, db_name, token=nedbd_token) # type: ignore[assignment]
|
|
317
324
|
self._backend = None
|
|
318
|
-
elif backend
|
|
325
|
+
elif backend == "dag" or (backend == "auto" and dag_path is not None):
|
|
326
|
+
# NOTE the `dag_path is not None`. Previously `auto` alone chose the
|
|
327
|
+
# embedded DAG the moment a platform wheel existed -- and an embedded
|
|
328
|
+
# DAG with no dag_path is IN-MEMORY. So wrap_redis(conn), which
|
|
329
|
+
# persisted through Redis Streams on the v1 AOF engine, silently
|
|
330
|
+
# stopped persisting as soon as pip installed a native wheel: same
|
|
331
|
+
# call site, different durability, no warning, discovered during an
|
|
332
|
+
# audit rather than a deploy.
|
|
333
|
+
#
|
|
334
|
+
# `auto` must never pick an engine LESS durable than the call site
|
|
335
|
+
# already had. Ask for the DAG explicitly (backend="dag") or give it
|
|
336
|
+
# somewhere to live (dag_path=) and you get it; stay on defaults and
|
|
337
|
+
# you keep Redis-Stream persistence.
|
|
319
338
|
# Embedded v2/v3 DAG (Rust native core). Falls back to v1 AOF
|
|
320
339
|
# when the platform wheel is absent (universal wheel).
|
|
321
340
|
try:
|
|
322
341
|
from .backends.dag import DagBackend
|
|
323
342
|
self._db = DagBackend(path=dag_path, tmk=dag_tmk) # type: ignore[assignment]
|
|
324
343
|
self._nedbd_mode = False
|
|
344
|
+
self.durable = dag_path is not None
|
|
325
345
|
self._backend = None # DAG persists itself; no Redis streams
|
|
326
346
|
except ImportError:
|
|
327
347
|
if backend == "dag":
|
|
@@ -586,8 +606,9 @@ class NEDBSurface:
|
|
|
586
606
|
evidence="redis_write",
|
|
587
607
|
confidence=1.0)
|
|
588
608
|
self._persist_last_op()
|
|
589
|
-
except Exception:
|
|
590
|
-
|
|
609
|
+
except Exception as e:
|
|
610
|
+
# Never breaks the Redis call, never invisible either.
|
|
611
|
+
self.note_shadow_error(e)
|
|
591
612
|
else:
|
|
592
613
|
# No mapping — raw tamper-evidence chain entry only
|
|
593
614
|
# (not NQL-queryable, but proves the write happened)
|
|
@@ -602,8 +623,21 @@ class NEDBSurface:
|
|
|
602
623
|
client="__shadow__",
|
|
603
624
|
evidence="redis_write")
|
|
604
625
|
self._persist_last_op()
|
|
626
|
+
except Exception as e:
|
|
627
|
+
self.note_shadow_error(e)
|
|
628
|
+
|
|
629
|
+
def note_shadow_error(self, exc: BaseException) -> None:
|
|
630
|
+
"""Record a swallowed shadow failure. Mirrors WrapSurface."""
|
|
631
|
+
self.shadow_errors = getattr(self, "shadow_errors", 0) + 1
|
|
632
|
+
self.last_shadow_error = f"{type(exc).__name__}: {exc}"
|
|
633
|
+
cb = getattr(self, "on_shadow_error", None)
|
|
634
|
+
if cb is not None:
|
|
635
|
+
try:
|
|
636
|
+
cb(exc)
|
|
605
637
|
except Exception:
|
|
606
638
|
pass
|
|
639
|
+
if getattr(self, "strict_shadow", False):
|
|
640
|
+
raise exc
|
|
607
641
|
|
|
608
642
|
# ── Full NEDB API ─────────────────────────────────────────────────────────
|
|
609
643
|
|
|
@@ -653,6 +687,11 @@ class NEDBSurface:
|
|
|
653
687
|
def verify(self) -> bool:
|
|
654
688
|
return self._db.verify()
|
|
655
689
|
|
|
690
|
+
# NOTE: @property. Without it `wrap_redis(...).nedb.head` handed back a
|
|
691
|
+
# bound method instead of the Merkle head string -- while `.seq` three
|
|
692
|
+
# lines below, and `.head` on every other wrapper, returned the value.
|
|
693
|
+
# Silent, because a bound method is truthy: `if surface.head:` passed.
|
|
694
|
+
@property
|
|
656
695
|
def head(self) -> str:
|
|
657
696
|
return self._db.head
|
|
658
697
|
|
|
@@ -158,56 +158,157 @@ class WrappedSqlite:
|
|
|
158
158
|
return _passthrough
|
|
159
159
|
|
|
160
160
|
def _execute(self, sql: str, params: tuple = ()):
|
|
161
|
-
"""execute() with post-
|
|
161
|
+
"""execute() with post-write shadowing of registered-table writes.
|
|
162
|
+
|
|
163
|
+
UPDATE and DELETE need the affected rowids captured BEFORE the
|
|
164
|
+
statement runs -- see _affected_rowids for why cursor.lastrowid
|
|
165
|
+
cannot be used for them.
|
|
166
|
+
"""
|
|
162
167
|
conn = object.__getattribute__(self, "_conn")
|
|
163
168
|
nedb = object.__getattribute__(self, "nedb")
|
|
169
|
+
|
|
170
|
+
head = sql.lstrip().upper()
|
|
171
|
+
shadowing = head.startswith(self._WRITE_PREFIXES) and nedb.shadow_writes
|
|
172
|
+
|
|
173
|
+
pre_rowids = None
|
|
174
|
+
if shadowing and not head.startswith("INSERT"):
|
|
175
|
+
try:
|
|
176
|
+
pre_rowids = self._affected_rowids(nedb, sql, params)
|
|
177
|
+
except Exception as e:
|
|
178
|
+
nedb.note_shadow_error(e)
|
|
179
|
+
pre_rowids = None
|
|
180
|
+
|
|
164
181
|
cur = conn.execute(sql, params)
|
|
165
182
|
|
|
166
183
|
try:
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
if shadowing:
|
|
185
|
+
self._shadow_sql(nedb, sql, cur, pre_rowids)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
# Must never break the host call — but must never be invisible
|
|
188
|
+
# either. Counted on nedb.shadow_errors, reported through
|
|
189
|
+
# nedb.on_shadow_error, and re-raised when strict_shadow is set.
|
|
190
|
+
nedb.note_shadow_error(e)
|
|
172
191
|
return cur
|
|
173
192
|
|
|
174
|
-
def
|
|
193
|
+
def _affected_rowids(self, nedb, sql: str, params: tuple):
|
|
194
|
+
"""Rowids an UPDATE/DELETE is about to touch, resolved before it runs.
|
|
195
|
+
|
|
196
|
+
`cursor.lastrowid` is ONLY meaningful after an INSERT. After an UPDATE
|
|
197
|
+
or DELETE it still holds the id of the last row *inserted* on that
|
|
198
|
+
connection, so shadowing off it recorded a completely unrelated row --
|
|
199
|
+
a FALSE provenance record, which is worse than none at all.
|
|
200
|
+
|
|
201
|
+
The affected set is therefore resolved up front with
|
|
202
|
+
`SELECT rowid FROM <table> WHERE <same predicate>`.
|
|
203
|
+
|
|
204
|
+
Placeholders: for UPDATE, some `?` may live in the SET clause, so the
|
|
205
|
+
leading params belonging to SET are skipped and only the remainder is
|
|
206
|
+
bound to the WHERE. Anything that cannot be parsed with confidence
|
|
207
|
+
raises, and the caller records a COUNTED shadow error rather than
|
|
208
|
+
inventing a row.
|
|
209
|
+
"""
|
|
210
|
+
import re
|
|
211
|
+
mapping = self._mapping_for_sql(nedb, sql)
|
|
212
|
+
if mapping is None:
|
|
213
|
+
return None
|
|
175
214
|
conn = object.__getattribute__(self, "_conn")
|
|
176
|
-
table =
|
|
215
|
+
table = mapping.pattern
|
|
216
|
+
|
|
217
|
+
m = re.search(r"\bWHERE\b(.*)$", sql, re.IGNORECASE | re.DOTALL)
|
|
218
|
+
if not m:
|
|
219
|
+
# No predicate: an unqualified UPDATE/DELETE touches every row.
|
|
220
|
+
return [r[0] for r in conn.execute(f'SELECT rowid FROM "{table}"')]
|
|
221
|
+
|
|
222
|
+
where = m.group(1)
|
|
223
|
+
where_params = params or ()
|
|
224
|
+
if sql.lstrip().upper().startswith("UPDATE") and where_params:
|
|
225
|
+
set_part = sql[:m.start()]
|
|
226
|
+
n_set = set_part.count("?")
|
|
227
|
+
if n_set > len(where_params):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
"cannot attribute placeholders between SET and WHERE; "
|
|
230
|
+
"refusing to shadow a row that may be the wrong one")
|
|
231
|
+
where_params = tuple(where_params)[n_set:]
|
|
232
|
+
|
|
233
|
+
rows = conn.execute(
|
|
234
|
+
f'SELECT rowid FROM "{table}" WHERE {where}', where_params)
|
|
235
|
+
return [r[0] for r in rows]
|
|
236
|
+
|
|
237
|
+
@staticmethod
|
|
238
|
+
def _mapping_for_sql(nedb, sql: str):
|
|
177
239
|
for m in nedb._mappings:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
240
|
+
if m.pattern.lower() in sql.lower():
|
|
241
|
+
return m
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _shadow_sql(self, nedb: SqliteSurface, sql: str, cur: sqlite3.Cursor,
|
|
245
|
+
pre_rowids=None) -> None:
|
|
246
|
+
"""Mirror a write into the SAME collection backfill() writes to.
|
|
247
|
+
|
|
248
|
+
Two defects lived here, and the second hid the first.
|
|
249
|
+
|
|
250
|
+
1. ``cols = [d[0] for d in cur.description] or [...]`` -- sqlite3 sets
|
|
251
|
+
``cursor.description`` to **None** after an INSERT (it is only
|
|
252
|
+
populated for SELECT). The list comprehension therefore raised
|
|
253
|
+
``TypeError`` before ``or`` could ever evaluate its fallback, and
|
|
254
|
+
``_execute`` swallowed it. Every automatic INSERT shadow silently
|
|
255
|
+
recorded nothing while ``verify()`` kept returning True.
|
|
256
|
+
|
|
257
|
+
2. Rows were written to ``__sql_shadow__`` while ``backfill()`` writes
|
|
258
|
+
to the registered collection. Even with (1) fixed, a user who ran
|
|
259
|
+
``register("rides", collection="ride")`` and queried ``FROM ride``
|
|
260
|
+
got the historical rows and none of the live ones -- a partial
|
|
261
|
+
answer, silently.
|
|
262
|
+
|
|
263
|
+
Both are fixed: the description is guarded, and every op lands in
|
|
264
|
+
``mapping.collection`` under the same id ``_host_scan`` yields
|
|
265
|
+
(``str(rowid)``), so an UPDATE supersedes the backfilled document
|
|
266
|
+
instead of accumulating beside it.
|
|
267
|
+
"""
|
|
268
|
+
conn = object.__getattribute__(self, "_conn")
|
|
269
|
+
mapping = self._mapping_for_sql(nedb, sql)
|
|
270
|
+
if mapping is None:
|
|
183
271
|
return
|
|
272
|
+
table = mapping.pattern
|
|
184
273
|
|
|
185
|
-
|
|
186
|
-
|
|
274
|
+
up = sql.lstrip().upper()
|
|
275
|
+
op = ("DELETE" if up.startswith("DELETE")
|
|
276
|
+
else "UPDATE" if up.startswith("UPDATE")
|
|
187
277
|
else "INSERT")
|
|
188
|
-
|
|
278
|
+
|
|
279
|
+
if op == "INSERT":
|
|
280
|
+
rowids = [cur.lastrowid]
|
|
281
|
+
else:
|
|
282
|
+
if pre_rowids is None:
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"{op} on {table!r}: affected rows could not be resolved, "
|
|
285
|
+
"nothing shadowed (see nedb.last_shadow_error)")
|
|
286
|
+
rowids = pre_rowids
|
|
189
287
|
|
|
190
288
|
if op == "DELETE":
|
|
191
|
-
# Tombstone
|
|
192
|
-
#
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
289
|
+
# Tombstone rather than a NEDB delete: deleting would drop history,
|
|
290
|
+
# and surviving history is the entire point. Written into the mapped
|
|
291
|
+
# collection so `FROM ride` shows the row as deleted rather than
|
|
292
|
+
# appearing to still exist.
|
|
293
|
+
for rid in rowids:
|
|
294
|
+
nedb.put(mapping.collection, str(rid),
|
|
295
|
+
{"_table": table, "_op": "DELETE", "_deleted": True,
|
|
296
|
+
"_rowid": str(rid)},
|
|
297
|
+
client="__shadow__")
|
|
196
298
|
return
|
|
197
299
|
|
|
198
|
-
# Read
|
|
199
|
-
|
|
300
|
+
# Read each affected row back post-write (inside the open transaction).
|
|
301
|
+
desc = conn.execute(f'SELECT * FROM "{table}" LIMIT 0').description
|
|
302
|
+
cols = [d[0] for d in desc]
|
|
303
|
+
for rid in rowids:
|
|
200
304
|
row = conn.execute(
|
|
201
|
-
f'SELECT * FROM "{table}" WHERE rowid = ?', (
|
|
305
|
+
f'SELECT * FROM "{table}" WHERE rowid = ?', (rid,)).fetchone()
|
|
202
306
|
if row is None:
|
|
203
|
-
|
|
204
|
-
cols = [d[0] for d in cur.description] or \
|
|
205
|
-
[d[0] for d in conn.execute(f'SELECT * FROM "{table}" LIMIT 0').description]
|
|
307
|
+
continue
|
|
206
308
|
doc = dict(zip(cols, row))
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
{**doc, "_table": table, "_op": op}, client="__shadow__")
|
|
309
|
+
nedb.put(mapping.collection, str(rid),
|
|
310
|
+
{**doc, "_table": table, "_op": op, "_source": "shadow"},
|
|
311
|
+
client="__shadow__")
|
|
211
312
|
|
|
212
313
|
def __repr__(self):
|
|
213
314
|
conn = object.__getattribute__(self, "_conn")
|
|
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
|
|
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
|