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.
Files changed (46) hide show
  1. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/PKG-INFO +1 -1
  2. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/pyproject.toml +1 -1
  3. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/__init__.py +1 -1
  4. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/_native.abi3.so +0 -0
  5. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/dag.py +10 -3
  6. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2 +0 -0
  7. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2-darwin-arm64 +0 -0
  8. nedb_engine-3.2.2/python/nedb/nedbd-v2-darwin-x64 +0 -0
  9. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/nedbd-v2.exe +0 -0
  10. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_core.py +65 -5
  11. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_redis.py +42 -3
  12. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_sqlite.py +132 -31
  13. nedb_engine-3.2.1/python/nedb/nedbd-v2-darwin-x64 +0 -0
  14. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/.gitignore +0 -0
  15. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/LICENSE +0 -0
  16. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/README.md +0 -0
  17. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/client/node/README.md +0 -0
  18. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/client/python/README.md +0 -0
  19. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/examples/agent-loop/README.md +0 -0
  20. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/examples/mini-chain/README.md +0 -0
  21. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/autoindex.py +0 -0
  22. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/__init__.py +0 -0
  23. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/backends/redis_backend.py +0 -0
  24. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/cascade.py +0 -0
  25. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/client.py +0 -0
  26. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/concurrent.py +0 -0
  27. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/crypto.py +0 -0
  28. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/engine.py +0 -0
  29. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/index.py +0 -0
  30. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/log.py +0 -0
  31. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/merkle.py +0 -0
  32. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/mongo.py +0 -0
  33. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/proof.py +0 -0
  34. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/query.py +0 -0
  35. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/redis_compat.py +0 -0
  36. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/relations.py +0 -0
  37. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/resp2.py +0 -0
  38. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/server.py +0 -0
  39. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/snapshot.py +0 -0
  40. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/sql.py +0 -0
  41. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/store.py +0 -0
  42. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_mongo.py +0 -0
  43. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_mysql.py +0 -0
  44. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/python/nedb/wrap_postgresql.py +0 -0
  45. {nedb_engine-3.2.1 → nedb_engine-3.2.2}/rust/crates/nedb-wrap/README.md +0 -0
  46. {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.1
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.1"
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"
@@ -102,4 +102,4 @@ __all__ = [
102
102
  "verify_proof", "fold_head",
103
103
  "_native", "__has_native__",
104
104
  ]
105
- __version__ = "3.2.1"
105
+ __version__ = "3.2.2"
@@ -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 importable."""
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__ # noqa: F401
38
- return True
44
+ from .. import __has_native__
45
+ return bool(__has_native__)
39
46
  except Exception:
40
47
  return False
41
48
 
@@ -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
- core = DagBackend(path=dag_path, tmk=dag_tmk)
68
- return core, None
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
- pass
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 in ("dag", "auto"):
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
- pass # shadow failures must never break the Redis surface call
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-commit shadowing of registered-table writes."""
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
- head = sql.lstrip().upper()
168
- if head.startswith(self._WRITE_PREFIXES) and nedb.shadow_writes:
169
- self._shadow_sql(nedb, sql, cur)
170
- except Exception:
171
- pass # shadow failures must never break the host call
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 _shadow_sql(self, nedb: SqliteSurface, sql: str, cur: sqlite3.Cursor) -> None:
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 = None
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
- t = m.pattern
179
- if t.lower() in sql.lower():
180
- table = t
181
- break
182
- if table is None:
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
- op = ("DELETE" if sql.upper().startswith("DELETE")
186
- else "UPDATE" if sql.upper().startswith("UPDATE")
274
+ up = sql.lstrip().upper()
275
+ op = ("DELETE" if up.startswith("DELETE")
276
+ else "UPDATE" if up.startswith("UPDATE")
187
277
  else "INSERT")
188
- rowid = cur.lastrowid
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: put a deletion marker (NEDB delete would remove history;
192
- # a marker keeps the causal chain and is NQL-queryable)
193
- nedb.put("__sql_shadow__", f"{table}:del:{rowid}",
194
- {"table": table, "rowid": str(rowid), "_op": "DELETE"},
195
- client="__shadow__")
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 the row back post-write (works inside the open transaction)
199
- try:
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 = ?', (rowid,)).fetchone()
305
+ f'SELECT * FROM "{table}" WHERE rowid = ?', (rid,)).fetchone()
202
306
  if row is None:
203
- return
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
- except sqlite3.Error:
208
- return
209
- nedb.put("__sql_shadow__", f"{table}:{rowid}",
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")
File without changes
File without changes
File without changes