synaptiq 2.0.2__tar.gz → 2.0.3__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.
- {synaptiq-2.0.2 → synaptiq-2.0.3}/PKG-INFO +1 -1
- {synaptiq-2.0.2 → synaptiq-2.0.3}/pyproject.toml +1 -1
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/__init__.py +1 -1
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/embeddings/lazy_worker.py +49 -9
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/manifest.py +28 -3
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/storage/ladybug_backend.py +123 -18
- {synaptiq-2.0.2 → synaptiq-2.0.3}/README.md +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/__main__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/cli/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/cli/main.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/cli/update_check.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/config/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/config/ignore.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/config/languages.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/cypher_guard.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/daemon/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/daemon/lock.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/daemon/rwlock.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/daemon/socket_client.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/daemon/socket_server.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/diff.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/embeddings/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/embeddings/embedder.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/embeddings/text.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/graph/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/graph/graph.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/graph/model.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/calls.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/community.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/coupling.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/dead_code.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/heritage.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/imports.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/incremental.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/incremental_build.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/parser_phase.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/pipeline.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/processes.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/rest_linking.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/structure.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/symbol_lookup.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/types.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/walker.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/ingestion/watcher.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/memory.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/base.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/go_lang.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/python_lang.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/ruby_lang.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/parsers/typescript.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/resources.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/search/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/search/hybrid.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/search/pagerank.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/storage/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/core/storage/base.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/__init__.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/freshness.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/http_transport.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/resources.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/secret_scanner.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/server.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/suggest.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/token_budget.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/mcp/tools.py +0 -0
- {synaptiq-2.0.2 → synaptiq-2.0.3}/src/synaptiq/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synaptiq
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: Graph-powered code intelligence engine — indexes codebases into a knowledge graph, exposed via MCP tools for AI agents and a CLI for developers.
|
|
5
5
|
Keywords: code-intelligence,knowledge-graph,mcp,tree-sitter,static-analysis,dead-code,claude-code
|
|
6
6
|
Author: Stevica Canadi
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "synaptiq"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.3"
|
|
4
4
|
description = "Graph-powered code intelligence engine — indexes codebases into a knowledge graph, exposed via MCP tools for AI agents and a CLI for developers."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -342,6 +342,7 @@ def _encode_and_store(data_dir: Path, db_path: Path, started_at: str) -> None:
|
|
|
342
342
|
from synaptiq.core.embeddings.embedder import (
|
|
343
343
|
embed_graph,
|
|
344
344
|
embeddable_node_count,
|
|
345
|
+
partition_embeddings,
|
|
345
346
|
tier_from_meta,
|
|
346
347
|
)
|
|
347
348
|
|
|
@@ -369,16 +370,53 @@ def _encode_and_store(data_dir: Path, db_path: Path, started_at: str) -> None:
|
|
|
369
370
|
return
|
|
370
371
|
graph, previous = loaded
|
|
371
372
|
|
|
372
|
-
|
|
373
|
-
if total == 0:
|
|
373
|
+
if embeddable_node_count(graph) == 0:
|
|
374
374
|
_update_meta_embeddings(data_dir, 0, expect_anchor=anchor)
|
|
375
375
|
write_state(data_dir, "complete", done=0, total=0, started_at=started_at)
|
|
376
376
|
return
|
|
377
377
|
|
|
378
|
-
|
|
378
|
+
# Only the PENDING delta is actually encoded — embed_graph reuses every
|
|
379
|
+
# unchanged vector and runs the model on just the new/changed symbols. So
|
|
380
|
+
# report progress against the pending count, not the full embeddable-node
|
|
381
|
+
# count: a one-file change must read "encoding 3/12", never
|
|
382
|
+
# "encoding 0/26,909". partition_embeddings runs the same split
|
|
383
|
+
# embed_graph does but never loads the model (generate-text cost), so this
|
|
384
|
+
# is cheap next to the encode it precedes.
|
|
385
|
+
reused_vecs, pending = partition_embeddings(graph, previous, tier=tier.name)
|
|
386
|
+
reused = len(reused_vecs)
|
|
387
|
+
if pending == 0:
|
|
388
|
+
# Nothing new to encode (everything reused). Still store the reused
|
|
389
|
+
# set so meta reflects the true vector count, but there is no work to
|
|
390
|
+
# show progress for.
|
|
391
|
+
embeddings = reused_vecs
|
|
392
|
+
if _read_meta_timestamp(data_dir) != anchor:
|
|
393
|
+
continue
|
|
394
|
+
if _store_with_retry(db_path, embeddings):
|
|
395
|
+
_update_meta_embeddings(data_dir, len(embeddings), expect_anchor=anchor)
|
|
396
|
+
write_state(data_dir, "complete", done=0, total=0, started_at=started_at)
|
|
397
|
+
else:
|
|
398
|
+
write_state(
|
|
399
|
+
data_dir,
|
|
400
|
+
"deferred",
|
|
401
|
+
done=0,
|
|
402
|
+
total=0,
|
|
403
|
+
started_at=started_at,
|
|
404
|
+
detail="index locked; re-run `synaptiq analyze` to encode",
|
|
405
|
+
)
|
|
406
|
+
return
|
|
379
407
|
|
|
380
|
-
|
|
381
|
-
|
|
408
|
+
write_state(data_dir, "encoding", done=0, total=pending, started_at=started_at)
|
|
409
|
+
|
|
410
|
+
def _on_progress(done: int, _full: int) -> None:
|
|
411
|
+
# embed_graph reports done = reused + encoded-so-far against the full
|
|
412
|
+
# embeddable count; re-base onto the pending delta for honest status.
|
|
413
|
+
write_state(
|
|
414
|
+
data_dir,
|
|
415
|
+
"encoding",
|
|
416
|
+
done=max(0, min(done - reused, pending)),
|
|
417
|
+
total=pending,
|
|
418
|
+
started_at=started_at,
|
|
419
|
+
)
|
|
382
420
|
|
|
383
421
|
embeddings = embed_graph(
|
|
384
422
|
graph, tier=tier.name, previous=previous, progress_callback=_on_progress
|
|
@@ -393,16 +431,18 @@ def _encode_and_store(data_dir: Path, db_path: Path, started_at: str) -> None:
|
|
|
393
431
|
)
|
|
394
432
|
continue
|
|
395
433
|
|
|
396
|
-
# (f) Store under the single-writer lock (retry → deferred).
|
|
434
|
+
# (f) Store under the single-writer lock (retry → deferred). Progress is
|
|
435
|
+
# reported against the pending delta (see above), so a completed encode
|
|
436
|
+
# reads "pending/pending".
|
|
397
437
|
if _store_with_retry(db_path, embeddings):
|
|
398
438
|
_update_meta_embeddings(data_dir, len(embeddings), expect_anchor=anchor)
|
|
399
|
-
write_state(data_dir, "complete", done=
|
|
439
|
+
write_state(data_dir, "complete", done=pending, total=pending, started_at=started_at)
|
|
400
440
|
else:
|
|
401
441
|
write_state(
|
|
402
442
|
data_dir,
|
|
403
443
|
"deferred",
|
|
404
|
-
done=
|
|
405
|
-
total=
|
|
444
|
+
done=pending,
|
|
445
|
+
total=pending,
|
|
406
446
|
started_at=started_at,
|
|
407
447
|
detail="index locked; re-run `synaptiq analyze` to encode",
|
|
408
448
|
)
|
|
@@ -29,6 +29,7 @@ from __future__ import annotations
|
|
|
29
29
|
|
|
30
30
|
import hashlib
|
|
31
31
|
import json
|
|
32
|
+
import logging
|
|
32
33
|
from dataclasses import dataclass, field
|
|
33
34
|
from datetime import datetime, timezone
|
|
34
35
|
from typing import Any
|
|
@@ -36,6 +37,8 @@ from typing import Any
|
|
|
36
37
|
from synaptiq.core.graph.graph import KnowledgeGraph
|
|
37
38
|
from synaptiq.core.graph.model import GraphNode, NodeLabel, RelType
|
|
38
39
|
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
39
42
|
# ---------------------------------------------------------------------------
|
|
40
43
|
# Versioning
|
|
41
44
|
# ---------------------------------------------------------------------------
|
|
@@ -494,9 +497,25 @@ def _parse_file_manifest_row(row: list[Any]) -> FileManifest | None:
|
|
|
494
497
|
path = row[0]
|
|
495
498
|
content = row[1] or ""
|
|
496
499
|
language = row[2] or ""
|
|
497
|
-
symbol_ids
|
|
498
|
-
|
|
499
|
-
|
|
500
|
+
# ``symbol_ids`` / ``symbol_sigs`` / ``out_edges`` are ALWAYS serialized
|
|
501
|
+
# as non-empty JSON (``"[]"`` / ``"{}"``; see serialize_file_manifest), so
|
|
502
|
+
# a NULL/empty value in any of them is not "no symbols" — it means the
|
|
503
|
+
# storage layer silently DROPPED the field (the ladybug bug where large
|
|
504
|
+
# string columns are corrupted when written into a node table that
|
|
505
|
+
# previously had rows DETACH DELETEd, which 2.0.3 fixes at the source).
|
|
506
|
+
# Treating that as empty provenance is the worst outcome: invisible
|
|
507
|
+
# incremental blindness. So fail LOUD — a dropped field poisons the row
|
|
508
|
+
# (caller -> None -> full rebuild), never silent partial provenance.
|
|
509
|
+
if not row[3] or not row[4] or not row[5]:
|
|
510
|
+
logger.warning(
|
|
511
|
+
"manifest row for %r has empty symbol provenance (dropped by "
|
|
512
|
+
"storage); treating the whole manifest as corrupt -> full rebuild",
|
|
513
|
+
path,
|
|
514
|
+
)
|
|
515
|
+
return None
|
|
516
|
+
symbol_ids = json.loads(row[3])
|
|
517
|
+
symbol_sigs = json.loads(row[4])
|
|
518
|
+
raw_edges = json.loads(row[5])
|
|
500
519
|
# ``unresolved_imports`` was added after the first manifest schema; a row
|
|
501
520
|
# written before it (or a NULL from CSV COPY) has fewer than 7 cols —
|
|
502
521
|
# tolerate that with an empty list rather than treating it as corrupt.
|
|
@@ -600,6 +619,12 @@ def load_manifest_from_rows(
|
|
|
600
619
|
for row in file_rows:
|
|
601
620
|
fm = _parse_file_manifest_row(row)
|
|
602
621
|
if fm is None:
|
|
622
|
+
path = row[0] if row else "<unknown>"
|
|
623
|
+
logger.warning(
|
|
624
|
+
"corrupt manifest file row (%r); manifest is untrustworthy -> "
|
|
625
|
+
"forcing a full rebuild",
|
|
626
|
+
path,
|
|
627
|
+
)
|
|
603
628
|
return None
|
|
604
629
|
files[fm.path] = fm
|
|
605
630
|
return Manifest(index=index, files=files)
|
|
@@ -48,6 +48,7 @@ except ImportError: # pragma: no cover - exercised in CSV-only environments
|
|
|
48
48
|
from synaptiq.core.graph.graph import KnowledgeGraph
|
|
49
49
|
from synaptiq.core.graph.model import GraphNode, GraphRelationship, NodeLabel
|
|
50
50
|
from synaptiq.core.ingestion.manifest import (
|
|
51
|
+
FILE_MANIFEST_COLUMNS,
|
|
51
52
|
Manifest,
|
|
52
53
|
build_manifest,
|
|
53
54
|
load_manifest_from_rows,
|
|
@@ -2290,21 +2291,87 @@ class LadybugBackend:
|
|
|
2290
2291
|
except Exception:
|
|
2291
2292
|
logger.debug("manifest table creation skipped", exc_info=True)
|
|
2292
2293
|
|
|
2294
|
+
def _reset_manifest_file_table(self) -> None:
|
|
2295
|
+
"""DROP + re-CREATE the FileManifest table for FRESH string storage.
|
|
2296
|
+
|
|
2297
|
+
The crux of the 2.0.3 manifest-persistence fix. LadybugDB silently drops
|
|
2298
|
+
the values of large STRING columns when rows are written into a node
|
|
2299
|
+
table that previously had rows removed via ``DETACH DELETE`` — verified on
|
|
2300
|
+
a real 3,563-file repo where the SECOND manifest write lost 533 rows /
|
|
2301
|
+
5,397 symbol ids, size-correlated (the files with the most symbols first),
|
|
2302
|
+
*identically* through CSV COPY, in-memory Arrow COPY, and parameterized
|
|
2303
|
+
MERGE. The corruption lives in the table's reused string storage, not in
|
|
2304
|
+
any one writer; only never-deleted-from storage round-trips losslessly.
|
|
2305
|
+
``bulk_load`` stamps the first manifest into a pristine ``.rebuild`` table
|
|
2306
|
+
(always fine); the losses were in the SECOND write onward — the
|
|
2307
|
+
``stamp_full_manifest`` after coupling and every incremental
|
|
2308
|
+
consolidation reused the populated live table. Dropping and recreating
|
|
2309
|
+
gives every write a pristine table, which round-trips losslessly.
|
|
2310
|
+
"""
|
|
2311
|
+
assert self._conn is not None
|
|
2312
|
+
try:
|
|
2313
|
+
self._conn.execute(f"DROP TABLE IF EXISTS {_MANIFEST_FILE_TABLE}")
|
|
2314
|
+
except Exception:
|
|
2315
|
+
logger.debug("manifest file-table drop skipped", exc_info=True)
|
|
2316
|
+
self._conn.execute(_FILE_MANIFEST_DDL)
|
|
2317
|
+
|
|
2318
|
+
def _reset_manifest_index_table(self) -> None:
|
|
2319
|
+
"""DROP + re-CREATE the IndexManifest table (drops the prior singleton)."""
|
|
2320
|
+
assert self._conn is not None
|
|
2321
|
+
try:
|
|
2322
|
+
self._conn.execute(f"DROP TABLE IF EXISTS {_MANIFEST_INDEX_TABLE}")
|
|
2323
|
+
except Exception:
|
|
2324
|
+
logger.debug("manifest index-table drop skipped", exc_info=True)
|
|
2325
|
+
self._conn.execute(_INDEX_MANIFEST_DDL)
|
|
2326
|
+
|
|
2327
|
+
def _refresh_write_conn(self) -> None:
|
|
2328
|
+
"""Recreate the write connection to drop LadybugDB's stale caches after DDL.
|
|
2329
|
+
|
|
2330
|
+
LadybugDB caches prepared statements (keyed by query string) and table
|
|
2331
|
+
catalog entries per connection, and does NOT invalidate them when a table
|
|
2332
|
+
is ``DROP``ped and re-``CREATE``d underneath. Re-running a parameterized
|
|
2333
|
+
statement (the index-singleton ``CREATE``, the ``CREATE`` row fallback) or
|
|
2334
|
+
a ``MATCH``-based query against a just-reset table on the SAME connection
|
|
2335
|
+
then crashes with ``unordered_map::at: key not found`` /
|
|
2336
|
+
``Cannot find table catalog entry``. Manifest writes reuse one write
|
|
2337
|
+
connection across incremental consolidations and the daemon's lifetime, so
|
|
2338
|
+
after resetting the manifest tables we swap in a fresh connection whose
|
|
2339
|
+
caches are empty — the light, targeted analogue of what the ``bulk_load``
|
|
2340
|
+
swap gets for free by calling :meth:`initialize`. The DB handle is
|
|
2341
|
+
untouched (cheap); pending manifest DDL already auto-committed, so nothing
|
|
2342
|
+
is lost. ``self._prepared`` (this backend's own per-connection cache) is
|
|
2343
|
+
cleared for the same reason it is in :meth:`initialize`.
|
|
2344
|
+
"""
|
|
2345
|
+
assert self._db is not None
|
|
2346
|
+
old = self._conn
|
|
2347
|
+
self._conn = ladybug.Connection(self._db)
|
|
2348
|
+
self._prepared = {}
|
|
2349
|
+
if old is not None:
|
|
2350
|
+
try:
|
|
2351
|
+
old.close()
|
|
2352
|
+
except Exception:
|
|
2353
|
+
logger.debug("closing prior write connection failed", exc_info=True)
|
|
2354
|
+
|
|
2293
2355
|
def write_manifest(self, manifest: Manifest) -> None:
|
|
2294
2356
|
"""Persist *manifest* into the current DB, replacing any prior manifest.
|
|
2295
2357
|
|
|
2296
|
-
Full replace
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2358
|
+
Full replace via DROP + re-CREATE of both manifest tables (see
|
|
2359
|
+
:meth:`_reset_manifest_file_table` for why fresh storage is mandatory),
|
|
2360
|
+
then bulk-writes the per-file rows, then writes the index singleton
|
|
2361
|
+
**last** as a completion marker: resetting the index table removes the
|
|
2362
|
+
prior singleton, so throughout the rewrite there is no completion marker —
|
|
2363
|
+
a partial/crashed write leaves none, :meth:`read_manifest` reports "no
|
|
2364
|
+
manifest", and the caller does a full rebuild. Not wrapped in an explicit
|
|
2365
|
+
transaction (DDL auto-commits) — crash-safety rides the singleton-last
|
|
2366
|
+
marker and, for the ``bulk_load`` write, the ``.rebuild`` swap. Uses the
|
|
2302
2367
|
write connection, so callers serialize via the external lock.
|
|
2303
2368
|
"""
|
|
2304
2369
|
assert self._conn is not None
|
|
2305
|
-
self.
|
|
2306
|
-
self.
|
|
2307
|
-
|
|
2370
|
+
self._reset_manifest_file_table()
|
|
2371
|
+
self._reset_manifest_index_table()
|
|
2372
|
+
# Drop LadybugDB's per-connection caches staled by the resets above,
|
|
2373
|
+
# before any cached/parameterized DML runs on this connection.
|
|
2374
|
+
self._refresh_write_conn()
|
|
2308
2375
|
rows = [serialize_file_manifest(fm) for fm in manifest.files.values()]
|
|
2309
2376
|
if rows:
|
|
2310
2377
|
self._write_manifest_file_rows(rows)
|
|
@@ -2315,22 +2382,60 @@ class LadybugBackend:
|
|
|
2315
2382
|
)
|
|
2316
2383
|
|
|
2317
2384
|
def _write_manifest_file_rows(self, rows: list[list[Any]]) -> None:
|
|
2318
|
-
"""
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2385
|
+
"""Bulk-write FileManifest rows into the freshly-reset (empty) table.
|
|
2386
|
+
|
|
2387
|
+
The table is pristine (``write_manifest`` reset it), so this only ever
|
|
2388
|
+
INSERTs. Prefers the in-memory Arrow COPY (pyarrow is a core dep; native
|
|
2389
|
+
arbitrary-size strings, no temp file), falls back to CSV COPY, then to
|
|
2390
|
+
row-by-row ``CREATE`` for COPY-unavailable environments. ``CREATE`` — not
|
|
2391
|
+
``MERGE`` — because LadybugDB raises a stale-catalog error on a
|
|
2392
|
+
MATCH-based ``MERGE`` executed against a just-DROP+CREATEd table, and a
|
|
2393
|
+
fresh empty table never needs upsert semantics. Each fallback first
|
|
2394
|
+
re-resets the file table so a COPY that failed after partial progress
|
|
2395
|
+
cannot leave duplicate/torn rows behind.
|
|
2322
2396
|
"""
|
|
2323
2397
|
assert self._conn is not None
|
|
2398
|
+
if _HAS_PYARROW:
|
|
2399
|
+
try:
|
|
2400
|
+
self._write_manifest_file_rows_arrow(rows)
|
|
2401
|
+
return
|
|
2402
|
+
except Exception:
|
|
2403
|
+
logger.debug("manifest Arrow COPY failed; trying CSV COPY", exc_info=True)
|
|
2404
|
+
self._reset_manifest_file_table()
|
|
2405
|
+
self._refresh_write_conn()
|
|
2324
2406
|
try:
|
|
2325
2407
|
self._csv_copy(_MANIFEST_FILE_TABLE, rows)
|
|
2326
2408
|
return
|
|
2327
2409
|
except Exception:
|
|
2328
|
-
logger.debug("manifest CSV COPY failed; falling back to row
|
|
2410
|
+
logger.debug("manifest CSV COPY failed; falling back to row CREATE", exc_info=True)
|
|
2411
|
+
self._reset_manifest_file_table()
|
|
2412
|
+
self._refresh_write_conn()
|
|
2413
|
+
self._write_manifest_file_rows_create(rows)
|
|
2414
|
+
|
|
2415
|
+
def _write_manifest_file_rows_arrow(self, rows: list[list[Any]]) -> None:
|
|
2416
|
+
"""COPY FileManifest rows from an in-memory pyarrow table (native strings).
|
|
2417
|
+
|
|
2418
|
+
Columns are built in :data:`FILE_MANIFEST_COLUMNS` order — contractual
|
|
2419
|
+
with the DDL and the read query. Values are stored verbatim (no
|
|
2420
|
+
empty→NULL coercion): the JSON columns are never empty, and preserving
|
|
2421
|
+
them exactly keeps the reader's loud empty-provenance gate meaningful.
|
|
2422
|
+
"""
|
|
2423
|
+
assert self._conn is not None
|
|
2424
|
+
arrow_tbl = pa.table(
|
|
2425
|
+
{
|
|
2426
|
+
name: pa.array([row[i] for row in rows], type=pa.string())
|
|
2427
|
+
for i, name in enumerate(FILE_MANIFEST_COLUMNS)
|
|
2428
|
+
}
|
|
2429
|
+
)
|
|
2430
|
+
self._arrow_copy(_MANIFEST_FILE_TABLE, arrow_tbl)
|
|
2431
|
+
|
|
2432
|
+
def _write_manifest_file_rows_create(self, rows: list[list[Any]]) -> None:
|
|
2433
|
+
"""Row-by-row ``CREATE`` fallback for COPY-unavailable environments."""
|
|
2434
|
+
assert self._conn is not None
|
|
2329
2435
|
stmt = (
|
|
2330
|
-
f"
|
|
2331
|
-
"
|
|
2332
|
-
"
|
|
2333
|
-
"m.out_edges = $out_edges, m.unresolved_imports = $unresolved_imports"
|
|
2436
|
+
f"CREATE (m:{_MANIFEST_FILE_TABLE} {{path: $path, content_sha: $content_sha, "
|
|
2437
|
+
"language: $language, symbol_ids: $symbol_ids, symbol_sigs: $symbol_sigs, "
|
|
2438
|
+
"out_edges: $out_edges, unresolved_imports: $unresolved_imports}})"
|
|
2334
2439
|
)
|
|
2335
2440
|
for row in rows:
|
|
2336
2441
|
self._conn.execute(
|
|
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
|
|
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
|