codespine 1.0.4__tar.gz → 1.0.5__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.4 → codespine-1.0.5}/PKG-INFO +1 -1
- {codespine-1.0.4 → codespine-1.0.5}/codespine/__init__.py +1 -1
- {codespine-1.0.4 → codespine-1.0.5}/codespine/cli.py +15 -8
- {codespine-1.0.4 → codespine-1.0.5}/codespine/db/_cypher_compat.py +43 -1
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/PKG-INFO +1 -1
- {codespine-1.0.4 → codespine-1.0.5}/pyproject.toml +1 -1
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_cypher_compat.py +45 -0
- {codespine-1.0.4 → codespine-1.0.5}/LICENSE +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/README.md +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/community.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/context.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/coupling.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/crossmodule.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/deadcode.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/flow.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/impact.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/cache/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/cache/result_cache.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/config.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/db/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/db/duckdb_store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/db/schema.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/db/store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/diff/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/diff/branch_diff.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/guide.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/call_resolver.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/di_resolver.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/engine.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/java_parser.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/symbol_builder.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/mcp/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/mcp/server.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/noise/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/noise/blocklist.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/git_state.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/merge.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/bm25.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/fuzzy.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/hybrid.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/rrf.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/search/vector.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/router.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/__init__.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/git_hook.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/watcher.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/SOURCES.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/dependency_links.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/entry_points.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/requires.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/top_level.txt +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/gindex.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/setup.cfg +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_branch_diff_normalize.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_call_resolver.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_community_detection.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_deadcode.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_duckdb_store.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_index_and_hybrid.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_java_parser.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_multimodule_index.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_overlay.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_result_cache.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_search_ranking.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_sharding.py +0 -0
- {codespine-1.0.4 → codespine-1.0.5}/tests/test_store_recovery.py +0 -0
|
@@ -537,21 +537,28 @@ def analyse(path: str, full: bool, deep: bool, incremental_deep: bool, embed: bo
|
|
|
537
537
|
|
|
538
538
|
_phase("Analyzing git history...", "skipped (large repo; rerun with --deep)")
|
|
539
539
|
|
|
540
|
-
|
|
540
|
+
# Summary queries are best-effort: a translator miss or a transient
|
|
541
|
+
# DB error must never throw away a successful index.
|
|
542
|
+
def _safe_count(query: str) -> int:
|
|
543
|
+
try:
|
|
544
|
+
rows = root_shard_store.query_records(query)
|
|
545
|
+
return int(rows[0]["count"]) if rows else 0
|
|
546
|
+
except Exception as exc: # noqa: BLE001 - summary stats are non-critical
|
|
547
|
+
click.secho(f" (summary stat unavailable: {exc})", fg="yellow")
|
|
548
|
+
return 0
|
|
549
|
+
|
|
550
|
+
embeddings_generated = last_result.embeddings_generated if last_result else 0
|
|
551
|
+
vectors_stored = _safe_count(
|
|
541
552
|
"""
|
|
542
553
|
MATCH (s:Symbol)
|
|
543
554
|
WHERE s.embedding IS NOT NULL
|
|
544
555
|
RETURN count(s) as count
|
|
545
556
|
"""
|
|
546
|
-
)
|
|
547
|
-
embeddings_generated = last_result.embeddings_generated if last_result else 0
|
|
548
|
-
vectors_stored = int(vector_count[0]["count"]) if vector_count else embeddings_generated
|
|
557
|
+
) or embeddings_generated
|
|
549
558
|
_phase("Generating embeddings...", f"{vectors_stored} vectors stored")
|
|
550
559
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
symbols = int(symbol_count[0]["count"]) if symbol_count else 0
|
|
554
|
-
edges = int(edge_count[0]["count"]) if edge_count else 0
|
|
560
|
+
symbols = _safe_count("MATCH (s:Symbol) RETURN count(s) as count")
|
|
561
|
+
edges = _safe_count("MATCH ()-[r]->() RETURN count(r) as count")
|
|
555
562
|
elapsed = time.perf_counter() - started
|
|
556
563
|
|
|
557
564
|
if not embed:
|
|
@@ -77,7 +77,47 @@ def translate(cypher: str, params: dict[str, Any] | None = None) -> tuple[str, d
|
|
|
77
77
|
# Internal translation pipeline
|
|
78
78
|
# ---------------------------------------------------------------------------
|
|
79
79
|
|
|
80
|
+
_ALL_EDGE_TABLES = (
|
|
81
|
+
"calls",
|
|
82
|
+
"references_type",
|
|
83
|
+
"injects",
|
|
84
|
+
"binds_interface",
|
|
85
|
+
"community_members",
|
|
86
|
+
"flow_members",
|
|
87
|
+
"co_changed_with",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _translate_anonymous_edge_count(cypher: str) -> str | None:
|
|
92
|
+
"""Handle `MATCH ()-[r]->() RETURN count(r) [as X]`.
|
|
93
|
+
|
|
94
|
+
Anonymous edge patterns carry no labels, so the generic translator
|
|
95
|
+
cannot derive a FROM table. We special-case the count-all-edges
|
|
96
|
+
pattern by unioning row-counts across every edge table in the
|
|
97
|
+
schema, which is what CodeSpine actually asks for.
|
|
98
|
+
"""
|
|
99
|
+
q = re.sub(r"\s+", " ", cypher.strip())
|
|
100
|
+
# Accept MATCH ()-[r]->() RETURN count(r) [as alias]
|
|
101
|
+
m = re.match(
|
|
102
|
+
r"(?i)MATCH\s*\(\s*\)\s*-\s*\[\s*(\w+)?\s*\]\s*->\s*\(\s*\)\s*"
|
|
103
|
+
r"RETURN\s+count\s*\(\s*\*?\w*\s*\)\s*(?:as\s+(\w+))?\s*$",
|
|
104
|
+
q,
|
|
105
|
+
)
|
|
106
|
+
if not m:
|
|
107
|
+
return None
|
|
108
|
+
alias = m.group(2) or "count"
|
|
109
|
+
unions = " UNION ALL ".join(
|
|
110
|
+
f"SELECT COUNT(*) AS c FROM {tbl}" for tbl in _ALL_EDGE_TABLES
|
|
111
|
+
)
|
|
112
|
+
return f"SELECT COALESCE(SUM(c), 0) AS {alias} FROM ({unions}) t"
|
|
113
|
+
|
|
114
|
+
|
|
80
115
|
def _translate(cypher: str) -> str:
|
|
116
|
+
# Fast-path: anonymous edge-count query used by `analyse` summary.
|
|
117
|
+
special = _translate_anonymous_edge_count(cypher)
|
|
118
|
+
if special is not None:
|
|
119
|
+
return special
|
|
120
|
+
|
|
81
121
|
q = re.sub(r"\s+", " ", cypher.strip())
|
|
82
122
|
|
|
83
123
|
# Collect node aliases before we start mangling the string
|
|
@@ -248,7 +288,9 @@ def _translate(cypher: str) -> str:
|
|
|
248
288
|
if tbl not in {et.split()[0] for et in edge_from}
|
|
249
289
|
or any(alias in ef for ef in edge_from)]
|
|
250
290
|
# Deduplicate: edge_from entries are already included via aliases
|
|
251
|
-
|
|
291
|
+
# Fallback: empty DuckDB-valid relation so an un-matched pattern
|
|
292
|
+
# degrades to zero rows instead of crashing with "table dual missing".
|
|
293
|
+
from_str = ", ".join(from_parts) if from_parts else "(SELECT 1 WHERE 1=0) _empty(x)"
|
|
252
294
|
|
|
253
295
|
# ----------------------------------------------------------------
|
|
254
296
|
# 7. Transform WHERE conditions
|
|
@@ -301,3 +301,48 @@ def test_no_return_gives_star():
|
|
|
301
301
|
sql = _translate("MATCH (n:File)")
|
|
302
302
|
assert "SELECT" in sql
|
|
303
303
|
assert "files" in sql
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Anonymous edge-count pattern (v1.0.5 regression)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_anonymous_edge_count():
|
|
312
|
+
"""`MATCH ()-[r]->() RETURN count(r) as count` — the query used by
|
|
313
|
+
`codespine analyse` to report total edges — must translate to a
|
|
314
|
+
DuckDB-valid query instead of falling through to `FROM dual`.
|
|
315
|
+
"""
|
|
316
|
+
sql = _translate("MATCH ()-[r]->() RETURN count(r) as count")
|
|
317
|
+
# Must reference real edge tables, not the Oracle-style `dual`.
|
|
318
|
+
assert "dual" not in sql.lower()
|
|
319
|
+
assert "calls" in sql
|
|
320
|
+
assert "references_type" in sql
|
|
321
|
+
assert "injects" in sql
|
|
322
|
+
assert "binds_interface" in sql
|
|
323
|
+
assert "community_members" in sql
|
|
324
|
+
assert "flow_members" in sql
|
|
325
|
+
assert "co_changed_with" in sql
|
|
326
|
+
# Alias should survive so callers can read row["count"].
|
|
327
|
+
assert "AS count" in sql or "as count" in sql
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_anonymous_edge_count_no_alias():
|
|
331
|
+
sql = _translate("MATCH ()-[r]->() RETURN count(r)")
|
|
332
|
+
assert "dual" not in sql.lower()
|
|
333
|
+
assert "calls" in sql
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_anonymous_edge_count_unnamed_rel():
|
|
337
|
+
# `MATCH ()-[]->()` (no rel variable) should also be handled
|
|
338
|
+
sql = _translate("MATCH ()-[]->() RETURN count(*) as count")
|
|
339
|
+
assert "dual" not in sql.lower()
|
|
340
|
+
assert "calls" in sql
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_unmatched_pattern_uses_safe_fallback():
|
|
344
|
+
"""If translator can't derive a FROM table, emit an empty DuckDB
|
|
345
|
+
relation rather than Oracle's `dual`.
|
|
346
|
+
"""
|
|
347
|
+
sql = _translate("MATCH (x:NotARealLabel) RETURN x.id")
|
|
348
|
+
assert "dual" not in sql.lower()
|
|
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
|