codespine 1.0.3__tar.gz → 1.0.4__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.
- {codespine-1.0.3 → codespine-1.0.4}/PKG-INFO +1 -1
- {codespine-1.0.3 → codespine-1.0.4}/codespine/__init__.py +1 -1
- {codespine-1.0.3 → codespine-1.0.4}/codespine/cli.py +35 -25
- {codespine-1.0.3 → codespine-1.0.4}/codespine/sharding/store.py +21 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/PKG-INFO +1 -1
- {codespine-1.0.3 → codespine-1.0.4}/pyproject.toml +1 -1
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_duckdb_store.py +39 -0
- {codespine-1.0.3 → codespine-1.0.4}/LICENSE +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/README.md +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/community.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/context.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/coupling.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/crossmodule.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/deadcode.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/flow.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/analysis/impact.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/cache/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/cache/result_cache.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/config.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/db/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/db/_cypher_compat.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/db/duckdb_store.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/db/schema.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/db/store.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/diff/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/diff/branch_diff.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/guide.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/call_resolver.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/di_resolver.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/engine.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/java_parser.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/mcp/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/mcp/server.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/noise/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/noise/blocklist.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/overlay/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/overlay/git_state.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/overlay/merge.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/overlay/store.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/bm25.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/fuzzy.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/hybrid.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/rrf.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/search/vector.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/sharding/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/sharding/router.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/watch/__init__.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/watch/git_hook.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine/watch/watcher.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/SOURCES.txt +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/requires.txt +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/codespine.egg-info/top_level.txt +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/gindex.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/setup.cfg +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_call_resolver.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_community_detection.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_cypher_compat.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_deadcode.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_index_and_hybrid.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_java_parser.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_multimodule_index.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_overlay.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_result_cache.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_search_ranking.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_sharding.py +0 -0
- {codespine-1.0.3 → codespine-1.0.4}/tests/test_store_recovery.py +0 -0
|
@@ -22,7 +22,6 @@ from codespine.analysis.deadcode import detect_dead_code
|
|
|
22
22
|
from codespine.analysis.flow import trace_execution_flows
|
|
23
23
|
from codespine.analysis.impact import analyze_impact
|
|
24
24
|
from codespine.config import SETTINGS
|
|
25
|
-
from codespine.db.store import GraphStore
|
|
26
25
|
from codespine.sharding import ShardedGraphStore, ShardRouter
|
|
27
26
|
from codespine.diff.branch_diff import compare_branches
|
|
28
27
|
from codespine.indexer.engine import JavaIndexer
|
|
@@ -56,6 +55,17 @@ def _current_repo_path() -> str:
|
|
|
56
55
|
return os.getcwd()
|
|
57
56
|
|
|
58
57
|
|
|
58
|
+
def _open_store(read_only: bool = True) -> ShardedGraphStore:
|
|
59
|
+
"""Open the sharded store with the backend configured in SETTINGS.
|
|
60
|
+
|
|
61
|
+
Every CLI command must go through this helper so the correct backend
|
|
62
|
+
(DuckDB or KùzuDB) is selected transparently. Direct ``GraphStore(...)``
|
|
63
|
+
calls were tied to the legacy single-DB KùzuDB layout and will fail on
|
|
64
|
+
any machine running the default DuckDB backend with sharded storage.
|
|
65
|
+
"""
|
|
66
|
+
return ShardedGraphStore(read_only=read_only)
|
|
67
|
+
|
|
68
|
+
|
|
59
69
|
def _db_size_bytes(path: str) -> int:
|
|
60
70
|
if os.path.isfile(path):
|
|
61
71
|
return os.path.getsize(path)
|
|
@@ -584,7 +594,7 @@ def analyse(path: str, full: bool, deep: bool, incremental_deep: bool, embed: bo
|
|
|
584
594
|
@click.option("--json", "as_json", is_flag=True)
|
|
585
595
|
def search(query: str, k: int, as_json: bool) -> None:
|
|
586
596
|
"""Hybrid search (BM25 + vector + fuzzy + RRF)."""
|
|
587
|
-
store =
|
|
597
|
+
store = _open_store(read_only=True)
|
|
588
598
|
results = hybrid_search(store, query, k=k)
|
|
589
599
|
_echo_json(results, as_json)
|
|
590
600
|
|
|
@@ -595,7 +605,7 @@ def search(query: str, k: int, as_json: bool) -> None:
|
|
|
595
605
|
@click.option("--json", "as_json", is_flag=True)
|
|
596
606
|
def context(query: str, max_depth: int, as_json: bool) -> None:
|
|
597
607
|
"""Get one-shot symbol context: search + impact + community + flows."""
|
|
598
|
-
store =
|
|
608
|
+
store = _open_store(read_only=True)
|
|
599
609
|
result = build_symbol_context(store, query, max_depth=max_depth)
|
|
600
610
|
_echo_json(result, as_json)
|
|
601
611
|
|
|
@@ -606,7 +616,7 @@ def context(query: str, max_depth: int, as_json: bool) -> None:
|
|
|
606
616
|
@click.option("--json", "as_json", is_flag=True)
|
|
607
617
|
def impact(symbol: str, max_depth: int, as_json: bool) -> None:
|
|
608
618
|
"""Impact analysis grouped by depth with confidence scores."""
|
|
609
|
-
store =
|
|
619
|
+
store = _open_store(read_only=True)
|
|
610
620
|
result = analyze_impact(store, symbol, max_depth=max_depth)
|
|
611
621
|
_echo_json(result, as_json)
|
|
612
622
|
|
|
@@ -616,7 +626,7 @@ def impact(symbol: str, max_depth: int, as_json: bool) -> None:
|
|
|
616
626
|
@click.option("--json", "as_json", is_flag=True)
|
|
617
627
|
def deadcode(limit: int, as_json: bool) -> None:
|
|
618
628
|
"""Detect dead code candidates with Java-aware exemptions."""
|
|
619
|
-
store =
|
|
629
|
+
store = _open_store(read_only=True)
|
|
620
630
|
result = detect_dead_code(store, limit=limit)
|
|
621
631
|
_echo_json(result, as_json)
|
|
622
632
|
|
|
@@ -627,7 +637,7 @@ def deadcode(limit: int, as_json: bool) -> None:
|
|
|
627
637
|
@click.option("--json", "as_json", is_flag=True)
|
|
628
638
|
def flow(entry_symbol: str | None, max_depth: int, as_json: bool) -> None:
|
|
629
639
|
"""Trace execution flows from detected entry points."""
|
|
630
|
-
store =
|
|
640
|
+
store = _open_store(read_only=True)
|
|
631
641
|
result = trace_execution_flows(store, entry_symbol=entry_symbol, max_depth=max_depth)
|
|
632
642
|
_echo_json(result, as_json)
|
|
633
643
|
|
|
@@ -637,7 +647,7 @@ def flow(entry_symbol: str | None, max_depth: int, as_json: bool) -> None:
|
|
|
637
647
|
@click.option("--json", "as_json", is_flag=True)
|
|
638
648
|
def community(symbol: str | None, as_json: bool) -> None:
|
|
639
649
|
"""Detect communities or lookup community for a symbol."""
|
|
640
|
-
store =
|
|
650
|
+
store = _open_store(read_only=False)
|
|
641
651
|
detect_communities(store)
|
|
642
652
|
if symbol:
|
|
643
653
|
_echo_json(symbol_community(store, symbol), as_json)
|
|
@@ -655,7 +665,7 @@ def community(symbol: str | None, as_json: bool) -> None:
|
|
|
655
665
|
@click.option("--json", "as_json", is_flag=True)
|
|
656
666
|
def coupling(days: int, min_strength: float, min_cochanges: int, as_json: bool) -> None:
|
|
657
667
|
"""Compute and query git change coupling."""
|
|
658
|
-
store =
|
|
668
|
+
store = _open_store(read_only=False)
|
|
659
669
|
project = store.query_records("MATCH (p:Project) RETURN p.id as id LIMIT 1")
|
|
660
670
|
project_id = project[0]["id"] if project else os.path.basename(os.getcwd())
|
|
661
671
|
compute_coupling(store, os.getcwd(), project_id, days=days, min_strength=min_strength, min_cochanges=min_cochanges)
|
|
@@ -681,7 +691,7 @@ def coupling(days: int, min_strength: float, min_cochanges: int, as_json: bool)
|
|
|
681
691
|
@click.option("--promote-on-commit/--no-promote-on-commit", default=True, show_default=True)
|
|
682
692
|
def watch(path: str, global_interval: int, overlay_debounce_ms: int, promote_on_commit: bool) -> None:
|
|
683
693
|
"""Live re-indexing and periodic global analysis refresh."""
|
|
684
|
-
store =
|
|
694
|
+
store = _open_store(read_only=False)
|
|
685
695
|
run_watch_mode(
|
|
686
696
|
store,
|
|
687
697
|
os.path.abspath(path),
|
|
@@ -720,12 +730,12 @@ def stats(as_json: bool, show_shards: bool) -> None:
|
|
|
720
730
|
def _project_store(pid: str):
|
|
721
731
|
return sg.shard(pid)
|
|
722
732
|
|
|
723
|
-
if not
|
|
733
|
+
if not all_projects_meta:
|
|
724
734
|
click.secho("No projects indexed yet. Run 'codespine analyse <path>'.", fg="yellow")
|
|
725
735
|
return
|
|
726
736
|
|
|
727
737
|
rows = []
|
|
728
|
-
for p in
|
|
738
|
+
for p in all_projects_meta:
|
|
729
739
|
pid = p["id"]
|
|
730
740
|
# Route each query to the project's owning shard.
|
|
731
741
|
ps = _project_store(pid)
|
|
@@ -813,7 +823,7 @@ def stats(as_json: bool, show_shards: bool) -> None:
|
|
|
813
823
|
@click.option("--json", "as_json", is_flag=True)
|
|
814
824
|
def list_projects(as_json: bool) -> None:
|
|
815
825
|
"""List indexed projects."""
|
|
816
|
-
store =
|
|
826
|
+
store = _open_store(read_only=True)
|
|
817
827
|
projects = store.query_records("MATCH (p:Project) RETURN p.id as id, p.path as path, p.language as language ORDER BY p.id")
|
|
818
828
|
_echo_json(projects, as_json)
|
|
819
829
|
|
|
@@ -837,7 +847,7 @@ def status(as_json: bool) -> None:
|
|
|
837
847
|
pid = int(f.read().strip())
|
|
838
848
|
except Exception:
|
|
839
849
|
pid = None
|
|
840
|
-
store =
|
|
850
|
+
store = _open_store(read_only=True)
|
|
841
851
|
overlay = get_overlay_status(store)
|
|
842
852
|
|
|
843
853
|
# Check for stale PID file
|
|
@@ -875,7 +885,7 @@ def status(as_json: bool) -> None:
|
|
|
875
885
|
@click.option("--json", "as_json", is_flag=True)
|
|
876
886
|
def overlay_status_cmd(project: str | None, as_json: bool) -> None:
|
|
877
887
|
"""Show dirty overlay status by project/module."""
|
|
878
|
-
store =
|
|
888
|
+
store = _open_store(read_only=True)
|
|
879
889
|
_echo_json(get_overlay_status(store, project=project), as_json)
|
|
880
890
|
|
|
881
891
|
|
|
@@ -884,7 +894,7 @@ def overlay_status_cmd(project: str | None, as_json: bool) -> None:
|
|
|
884
894
|
@click.option("--json", "as_json", is_flag=True)
|
|
885
895
|
def overlay_clear_cmd(project: str | None, as_json: bool) -> None:
|
|
886
896
|
"""Clear dirty overlay data without touching the committed base index."""
|
|
887
|
-
store =
|
|
897
|
+
store = _open_store(read_only=False)
|
|
888
898
|
result = {"cleared": clear_overlay(store, project=project)}
|
|
889
899
|
_echo_json(result, as_json)
|
|
890
900
|
|
|
@@ -894,7 +904,7 @@ def overlay_clear_cmd(project: str | None, as_json: bool) -> None:
|
|
|
894
904
|
@click.option("--json", "as_json", is_flag=True)
|
|
895
905
|
def overlay_promote_cmd(project: str | None, as_json: bool) -> None:
|
|
896
906
|
"""Promote dirty overlay changes into the committed base index now."""
|
|
897
|
-
store =
|
|
907
|
+
store = _open_store(read_only=False)
|
|
898
908
|
result = {"promoted": promote_overlay(store, project=project, require_head_change=False)}
|
|
899
909
|
_echo_json(result, as_json)
|
|
900
910
|
|
|
@@ -904,7 +914,7 @@ def overlay_promote_cmd(project: str | None, as_json: bool) -> None:
|
|
|
904
914
|
@click.option("--json", "as_json", is_flag=True)
|
|
905
915
|
def cypher(query: str, as_json: bool) -> None:
|
|
906
916
|
"""Run a raw Cypher query against the graph DB."""
|
|
907
|
-
store =
|
|
917
|
+
store = _open_store(read_only=True)
|
|
908
918
|
try:
|
|
909
919
|
result = store.query_records(query)
|
|
910
920
|
except Exception as exc:
|
|
@@ -948,7 +958,7 @@ def clear_project_cmd(project_id: str, allow_running: bool) -> None:
|
|
|
948
958
|
click.secho("Stop MCP first ('codespine stop') to modify index.", fg="yellow")
|
|
949
959
|
return
|
|
950
960
|
try:
|
|
951
|
-
store =
|
|
961
|
+
store = _open_store(read_only=False)
|
|
952
962
|
recs = store.query_records(
|
|
953
963
|
"MATCH (p:Project) WHERE p.id = $pid RETURN p.id as id, p.path as path",
|
|
954
964
|
{"pid": project_id},
|
|
@@ -974,7 +984,7 @@ def clear_project_cmd(project_id: str, allow_running: bool) -> None:
|
|
|
974
984
|
except OSError:
|
|
975
985
|
pass
|
|
976
986
|
# Update the read replica so read-only callers (stats, MCP) see the change.
|
|
977
|
-
|
|
987
|
+
store.snapshot_to_read_replica()
|
|
978
988
|
click.secho(f"Cleared project '{project_id}' (was at {project_path}).", fg="green")
|
|
979
989
|
|
|
980
990
|
|
|
@@ -991,12 +1001,12 @@ def clear_index_cmd(allow_running: bool) -> None:
|
|
|
991
1001
|
click.secho("Stop MCP first ('codespine stop') to modify index.", fg="yellow")
|
|
992
1002
|
return
|
|
993
1003
|
try:
|
|
994
|
-
store =
|
|
1004
|
+
store = _open_store(read_only=False)
|
|
995
1005
|
projects = store.query_records("MATCH (p:Project) RETURN p.id as id")
|
|
996
1006
|
except Exception:
|
|
997
1007
|
# DB is corrupted — can't even open it. Force-delete everything.
|
|
998
1008
|
click.secho("DB is corrupted. Running force-reset instead...", fg="yellow")
|
|
999
|
-
removed =
|
|
1009
|
+
removed = ShardedGraphStore(read_only=False).force_delete_all_data()
|
|
1000
1010
|
click.secho(f"Force-reset complete. {len(removed)} path(s) removed. Index is now empty.", fg="green")
|
|
1001
1011
|
return
|
|
1002
1012
|
try:
|
|
@@ -1004,7 +1014,7 @@ def clear_index_cmd(allow_running: bool) -> None:
|
|
|
1004
1014
|
except Exception as exc:
|
|
1005
1015
|
# rebuild_empty_db failed even with fallbacks — force-delete.
|
|
1006
1016
|
click.secho(f"rebuild failed ({exc}). Running force-reset...", fg="yellow")
|
|
1007
|
-
|
|
1017
|
+
store.force_delete_all_data()
|
|
1008
1018
|
click.secho("Force-reset complete. Index is now empty.", fg="green")
|
|
1009
1019
|
return
|
|
1010
1020
|
store.overlay_store.clear_all()
|
|
@@ -1017,7 +1027,7 @@ def clear_index_cmd(allow_running: bool) -> None:
|
|
|
1017
1027
|
pass
|
|
1018
1028
|
# Publish an empty read replica so that read-only callers (stats, MCP)
|
|
1019
1029
|
# immediately see the cleared state and the MCP daemon hot-reloads.
|
|
1020
|
-
|
|
1030
|
+
store.snapshot_to_read_replica()
|
|
1021
1031
|
click.secho(f"Cleared {len(projects)} project(s). Index is now empty.", fg="green")
|
|
1022
1032
|
|
|
1023
1033
|
|
|
@@ -1038,7 +1048,7 @@ def force_reset_cmd(force: bool) -> None:
|
|
|
1038
1048
|
):
|
|
1039
1049
|
click.echo("Aborted.")
|
|
1040
1050
|
return
|
|
1041
|
-
removed =
|
|
1051
|
+
removed = ShardedGraphStore(read_only=False).force_delete_all_data()
|
|
1042
1052
|
if removed:
|
|
1043
1053
|
for p in removed:
|
|
1044
1054
|
click.echo(f" removed: {p}")
|
|
@@ -1177,7 +1187,7 @@ def install_model() -> None:
|
|
|
1177
1187
|
@main.command("run-mcp", hidden=True)
|
|
1178
1188
|
def run_mcp() -> None:
|
|
1179
1189
|
"""Run MCP server in stdio mode."""
|
|
1180
|
-
store =
|
|
1190
|
+
store = _open_store(read_only=True)
|
|
1181
1191
|
mcp = build_mcp_server(store, repo_path_provider=_current_repo_path)
|
|
1182
1192
|
mcp.run()
|
|
1183
1193
|
|
|
@@ -299,6 +299,27 @@ class ShardedGraphStore:
|
|
|
299
299
|
removed.extend(store.force_delete_all_data())
|
|
300
300
|
return removed
|
|
301
301
|
|
|
302
|
+
def clear_analysis_artifacts(self) -> None:
|
|
303
|
+
"""Fan-out: clear analysis artifacts (communities, flows, dead code) on every shard."""
|
|
304
|
+
for store in self.all_shards():
|
|
305
|
+
try:
|
|
306
|
+
store.clear_analysis_artifacts()
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
LOGGER.warning("clear_analysis_artifacts failed on shard: %s", exc)
|
|
309
|
+
|
|
310
|
+
def rebuild_empty_db(self) -> None:
|
|
311
|
+
"""Fan-out: rebuild each shard as an empty database."""
|
|
312
|
+
for store in self.all_shards():
|
|
313
|
+
try:
|
|
314
|
+
store.rebuild_empty_db()
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
LOGGER.warning("rebuild_empty_db failed on shard: %s", exc)
|
|
317
|
+
|
|
318
|
+
def snapshot_to_read_replica(self, background: bool = False) -> bool:
|
|
319
|
+
"""Alias for ``snapshot_all`` — matches GraphStore's API."""
|
|
320
|
+
self.snapshot_all(background=background)
|
|
321
|
+
return True
|
|
322
|
+
|
|
302
323
|
def describe(self) -> dict:
|
|
303
324
|
"""Return a human-readable description of the shard topology."""
|
|
304
325
|
shard_info = []
|
|
@@ -446,3 +446,42 @@ def test_corrupt_file_at_db_path_is_replaced(tmp_path: Path):
|
|
|
446
446
|
store = DuckDBStore(db_path_override=db_path, snapshot_path_override=snap_path)
|
|
447
447
|
rows = store.query_records("SELECT * FROM projects")
|
|
448
448
|
assert rows == []
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_legacy_kuzu_dirs_at_both_paths_are_removed(tmp_path: Path):
|
|
452
|
+
"""Regression: both db and db_read as KùzuDB directories (the exact
|
|
453
|
+
scenario from the field bug in v1.0.2)."""
|
|
454
|
+
db_path = str(tmp_path / "db")
|
|
455
|
+
snap_path = str(tmp_path / "db_read")
|
|
456
|
+
|
|
457
|
+
# Simulate KùzuDB directories at BOTH paths
|
|
458
|
+
for p in (db_path, snap_path):
|
|
459
|
+
os.makedirs(p)
|
|
460
|
+
(Path(p) / "catalog.kz").write_bytes(b"\x00" * 64)
|
|
461
|
+
(Path(p) / "data.kz").write_bytes(b"\x00" * 1024)
|
|
462
|
+
|
|
463
|
+
# Read-only open: legacy code would pick snap, fail, fall back to db, fail, raise.
|
|
464
|
+
# New code pre-sanitizes both paths, then returns an in-memory empty DB.
|
|
465
|
+
store = DuckDBStore(read_only=True, db_path_override=db_path, snapshot_path_override=snap_path)
|
|
466
|
+
rows = store.query_records("SELECT * FROM projects")
|
|
467
|
+
assert rows == []
|
|
468
|
+
# Both paths should be gone
|
|
469
|
+
assert not os.path.exists(db_path)
|
|
470
|
+
assert not os.path.exists(snap_path)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_sharded_store_stats_flow_with_stale_kuzu_dirs(tmp_path: Path):
|
|
474
|
+
"""Regression: ShardedGraphStore.list_project_metadata() must not crash
|
|
475
|
+
when every shard path is a stale KùzuDB directory (the failing
|
|
476
|
+
'codespine stats' scenario)."""
|
|
477
|
+
shards_dir = tmp_path / "shards"
|
|
478
|
+
# Pre-create 4 shards with legacy KùzuDB-style directories at both paths
|
|
479
|
+
for i in range(4):
|
|
480
|
+
(shards_dir / str(i)).mkdir(parents=True)
|
|
481
|
+
(shards_dir / str(i) / "db").mkdir()
|
|
482
|
+
(shards_dir / str(i) / "db" / "catalog.kz").write_bytes(b"\x00" * 32)
|
|
483
|
+
(shards_dir / str(i) / "db_read").mkdir()
|
|
484
|
+
|
|
485
|
+
sg = ShardedGraphStore(read_only=True, shards_dir=str(shards_dir), backend="duckdb")
|
|
486
|
+
# This is what `codespine stats` does — it must not raise.
|
|
487
|
+
assert sg.list_project_metadata() == []
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|