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.
Files changed (73) hide show
  1. {codespine-1.0.4 → codespine-1.0.5}/PKG-INFO +1 -1
  2. {codespine-1.0.4 → codespine-1.0.5}/codespine/__init__.py +1 -1
  3. {codespine-1.0.4 → codespine-1.0.5}/codespine/cli.py +15 -8
  4. {codespine-1.0.4 → codespine-1.0.5}/codespine/db/_cypher_compat.py +43 -1
  5. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/PKG-INFO +1 -1
  6. {codespine-1.0.4 → codespine-1.0.5}/pyproject.toml +1 -1
  7. {codespine-1.0.4 → codespine-1.0.5}/tests/test_cypher_compat.py +45 -0
  8. {codespine-1.0.4 → codespine-1.0.5}/LICENSE +0 -0
  9. {codespine-1.0.4 → codespine-1.0.5}/README.md +0 -0
  10. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/__init__.py +0 -0
  11. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/community.py +0 -0
  12. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/context.py +0 -0
  13. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/coupling.py +0 -0
  14. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/crossmodule.py +0 -0
  15. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/deadcode.py +0 -0
  16. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/flow.py +0 -0
  17. {codespine-1.0.4 → codespine-1.0.5}/codespine/analysis/impact.py +0 -0
  18. {codespine-1.0.4 → codespine-1.0.5}/codespine/cache/__init__.py +0 -0
  19. {codespine-1.0.4 → codespine-1.0.5}/codespine/cache/result_cache.py +0 -0
  20. {codespine-1.0.4 → codespine-1.0.5}/codespine/config.py +0 -0
  21. {codespine-1.0.4 → codespine-1.0.5}/codespine/db/__init__.py +0 -0
  22. {codespine-1.0.4 → codespine-1.0.5}/codespine/db/duckdb_store.py +0 -0
  23. {codespine-1.0.4 → codespine-1.0.5}/codespine/db/schema.py +0 -0
  24. {codespine-1.0.4 → codespine-1.0.5}/codespine/db/store.py +0 -0
  25. {codespine-1.0.4 → codespine-1.0.5}/codespine/diff/__init__.py +0 -0
  26. {codespine-1.0.4 → codespine-1.0.5}/codespine/diff/branch_diff.py +0 -0
  27. {codespine-1.0.4 → codespine-1.0.5}/codespine/guide.py +0 -0
  28. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/__init__.py +0 -0
  29. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/call_resolver.py +0 -0
  30. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/di_resolver.py +0 -0
  31. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/engine.py +0 -0
  32. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/java_parser.py +0 -0
  33. {codespine-1.0.4 → codespine-1.0.5}/codespine/indexer/symbol_builder.py +0 -0
  34. {codespine-1.0.4 → codespine-1.0.5}/codespine/mcp/__init__.py +0 -0
  35. {codespine-1.0.4 → codespine-1.0.5}/codespine/mcp/server.py +0 -0
  36. {codespine-1.0.4 → codespine-1.0.5}/codespine/noise/__init__.py +0 -0
  37. {codespine-1.0.4 → codespine-1.0.5}/codespine/noise/blocklist.py +0 -0
  38. {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/__init__.py +0 -0
  39. {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/git_state.py +0 -0
  40. {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/merge.py +0 -0
  41. {codespine-1.0.4 → codespine-1.0.5}/codespine/overlay/store.py +0 -0
  42. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/__init__.py +0 -0
  43. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/bm25.py +0 -0
  44. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/fuzzy.py +0 -0
  45. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/hybrid.py +0 -0
  46. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/rrf.py +0 -0
  47. {codespine-1.0.4 → codespine-1.0.5}/codespine/search/vector.py +0 -0
  48. {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/__init__.py +0 -0
  49. {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/router.py +0 -0
  50. {codespine-1.0.4 → codespine-1.0.5}/codespine/sharding/store.py +0 -0
  51. {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/__init__.py +0 -0
  52. {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/git_hook.py +0 -0
  53. {codespine-1.0.4 → codespine-1.0.5}/codespine/watch/watcher.py +0 -0
  54. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/SOURCES.txt +0 -0
  55. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/dependency_links.txt +0 -0
  56. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/entry_points.txt +0 -0
  57. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/requires.txt +0 -0
  58. {codespine-1.0.4 → codespine-1.0.5}/codespine.egg-info/top_level.txt +0 -0
  59. {codespine-1.0.4 → codespine-1.0.5}/gindex.py +0 -0
  60. {codespine-1.0.4 → codespine-1.0.5}/setup.cfg +0 -0
  61. {codespine-1.0.4 → codespine-1.0.5}/tests/test_branch_diff_normalize.py +0 -0
  62. {codespine-1.0.4 → codespine-1.0.5}/tests/test_call_resolver.py +0 -0
  63. {codespine-1.0.4 → codespine-1.0.5}/tests/test_community_detection.py +0 -0
  64. {codespine-1.0.4 → codespine-1.0.5}/tests/test_deadcode.py +0 -0
  65. {codespine-1.0.4 → codespine-1.0.5}/tests/test_duckdb_store.py +0 -0
  66. {codespine-1.0.4 → codespine-1.0.5}/tests/test_index_and_hybrid.py +0 -0
  67. {codespine-1.0.4 → codespine-1.0.5}/tests/test_java_parser.py +0 -0
  68. {codespine-1.0.4 → codespine-1.0.5}/tests/test_multimodule_index.py +0 -0
  69. {codespine-1.0.4 → codespine-1.0.5}/tests/test_overlay.py +0 -0
  70. {codespine-1.0.4 → codespine-1.0.5}/tests/test_result_cache.py +0 -0
  71. {codespine-1.0.4 → codespine-1.0.5}/tests/test_search_ranking.py +0 -0
  72. {codespine-1.0.4 → codespine-1.0.5}/tests/test_sharding.py +0 -0
  73. {codespine-1.0.4 → codespine-1.0.5}/tests/test_store_recovery.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
1
  """CodeSpine package."""
2
2
 
3
3
  __all__ = ["__version__"]
4
- __version__ = "1.0.4"
4
+ __version__ = "1.0.5"
@@ -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
- vector_count = root_shard_store.query_records(
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
- symbol_count = root_shard_store.query_records("MATCH (s:Symbol) RETURN count(s) as count")
552
- edge_count = root_shard_store.query_records("MATCH ()-[r]->() RETURN count(r) as count")
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
- from_str = ", ".join(from_parts) if from_parts else "dual"
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codespine
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: Local Java code intelligence indexer backed by a graph database
5
5
  Author: CodeSpine contributors
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codespine"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "Local Java code intelligence indexer backed by a graph database"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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