codegraphcontext-rust 0.5.5__cp310-cp310-win_amd64.whl
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.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
# src/codegraphcontext/core/database_kuzu.py
|
|
2
|
+
"""
|
|
3
|
+
This module provides a thread-safe singleton manager for the KùzuDB database connection.
|
|
4
|
+
KùzuDB is an embedded graph database that is cross-platform (including Windows)
|
|
5
|
+
and requires no external server setup.
|
|
6
|
+
"""
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import threading
|
|
10
|
+
import re
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, Tuple, Dict, Any, List
|
|
14
|
+
|
|
15
|
+
from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
|
|
16
|
+
|
|
17
|
+
class KuzuDBManager:
|
|
18
|
+
"""
|
|
19
|
+
Manages the KùzuDB database connection as a singleton.
|
|
20
|
+
"""
|
|
21
|
+
_instance = None
|
|
22
|
+
_db = None
|
|
23
|
+
_conn = None
|
|
24
|
+
_lock = threading.Lock()
|
|
25
|
+
|
|
26
|
+
def __new__(cls, *args, **kwargs):
|
|
27
|
+
"""Standard singleton pattern implementation."""
|
|
28
|
+
if cls._instance is None:
|
|
29
|
+
with cls._lock:
|
|
30
|
+
if cls._instance is None:
|
|
31
|
+
cls._instance = super(KuzuDBManager, cls).__new__(cls)
|
|
32
|
+
return cls._instance
|
|
33
|
+
|
|
34
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
35
|
+
"""
|
|
36
|
+
Initializes the manager with default database path or explicit overrides.
|
|
37
|
+
"""
|
|
38
|
+
if hasattr(self, '_initialized') and self.db_path == db_path:
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
self._initialized = False
|
|
42
|
+
|
|
43
|
+
self.name = "kuzudb"
|
|
44
|
+
# Try to load from config manager
|
|
45
|
+
try:
|
|
46
|
+
from codegraphcontext.cli.config_manager import get_config_value
|
|
47
|
+
config_db_path = get_config_value('KUZUDB_PATH')
|
|
48
|
+
except Exception:
|
|
49
|
+
config_db_path = None
|
|
50
|
+
|
|
51
|
+
# Database path with fallback chain (Explicit > Env > Config/Default)
|
|
52
|
+
self.db_path = db_path or os.getenv(
|
|
53
|
+
'KUZUDB_PATH',
|
|
54
|
+
config_db_path or str(Path.home() / '.codegraphcontext' / 'global' / 'kuzudb')
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Ensure directory exists
|
|
58
|
+
os.makedirs(Path(self.db_path).parent, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
self._initialized = True
|
|
61
|
+
|
|
62
|
+
def get_driver(self):
|
|
63
|
+
"""
|
|
64
|
+
Gets the KùzuDB connection. Retries on file-lock errors.
|
|
65
|
+
"""
|
|
66
|
+
if self._conn is None:
|
|
67
|
+
with self._lock:
|
|
68
|
+
if self._conn is None:
|
|
69
|
+
import kuzu
|
|
70
|
+
max_retries = 5
|
|
71
|
+
for attempt in range(max_retries):
|
|
72
|
+
try:
|
|
73
|
+
info_logger(f"Initializing KùzuDB at {self.db_path}")
|
|
74
|
+
self._db = kuzu.Database(self.db_path)
|
|
75
|
+
self._conn = kuzu.Connection(self._db)
|
|
76
|
+
self._initialize_schema()
|
|
77
|
+
info_logger("KùzuDB connection established and schema verified")
|
|
78
|
+
break
|
|
79
|
+
except ImportError:
|
|
80
|
+
error_logger("KùzuDB is not installed. Run 'pip install kuzu'")
|
|
81
|
+
raise ValueError("KùzuDB missing.")
|
|
82
|
+
except Exception as e:
|
|
83
|
+
if "lock" in str(e).lower() and attempt < max_retries - 1:
|
|
84
|
+
wait = 0.5 * (2 ** attempt)
|
|
85
|
+
warning_logger(f"KùzuDB lock contention, retrying in {wait:.1f}s ({attempt+1}/{max_retries})...")
|
|
86
|
+
self._db = None
|
|
87
|
+
self._conn = None
|
|
88
|
+
time.sleep(wait)
|
|
89
|
+
else:
|
|
90
|
+
error_logger(f"Failed to initialize KùzuDB: {e}")
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
return KuzuDriverWrapper(self._conn)
|
|
94
|
+
|
|
95
|
+
def get_raw_connection(self):
|
|
96
|
+
"""Get the raw kuzu.Connection for bulk operations (e.g. Parquet COPY FROM).
|
|
97
|
+
|
|
98
|
+
Ensures the connection is initialized first via get_driver().
|
|
99
|
+
Returns the underlying kuzu.Connection, not the wrapper.
|
|
100
|
+
"""
|
|
101
|
+
if self._conn is None:
|
|
102
|
+
self.get_driver() # triggers lazy init
|
|
103
|
+
return self._conn
|
|
104
|
+
|
|
105
|
+
def _initialize_schema(self):
|
|
106
|
+
"""Creates Node and Rel tables if they don't exist."""
|
|
107
|
+
# Using a set of helper methods to define tables
|
|
108
|
+
# Kuzu's Cypher for checking tables can be limited,
|
|
109
|
+
# but we can wrap in try-except or check metadata.
|
|
110
|
+
|
|
111
|
+
node_tables = [
|
|
112
|
+
("Repository", "path STRING, name STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
|
|
113
|
+
("File", "path STRING, name STRING, relative_path STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
|
|
114
|
+
("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
|
|
115
|
+
("Module", "name STRING, lang STRING, full_import_name STRING, PRIMARY KEY (name)"),
|
|
116
|
+
# For types with composite keys (name, path, line_number), we use a 'uid'
|
|
117
|
+
("Function", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, cyclomatic_complexity INT64, context STRING, context_type STRING, class_context STRING, is_dependency BOOLEAN, decorators STRING[], args STRING[], PRIMARY KEY (uid)"),
|
|
118
|
+
("Class", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, decorators STRING[], PRIMARY KEY (uid)"),
|
|
119
|
+
("Variable", "uid STRING, name STRING, path STRING, line_number INT64, source STRING, docstring STRING, lang STRING, value STRING, context STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
120
|
+
("Trait", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
121
|
+
("Interface", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
122
|
+
("Macro", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
123
|
+
("Struct", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
124
|
+
("Enum", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
125
|
+
("Union", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
126
|
+
("Annotation", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
127
|
+
("Record", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
128
|
+
("Property", "uid STRING, name STRING, path STRING, line_number INT64, end_line INT64, source STRING, docstring STRING, lang STRING, is_dependency BOOLEAN, PRIMARY KEY (uid)"),
|
|
129
|
+
("Parameter", "uid STRING, name STRING, path STRING, function_line_number INT64, PRIMARY KEY (uid)")
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# rel_tables: list of (table_name, schema, use_group)
|
|
133
|
+
# use_group=True -> CREATE REL TABLE GROUP (for multi FROM..TO bindings)
|
|
134
|
+
# use_group=False -> CREATE REL TABLE (single binding)
|
|
135
|
+
rel_tables = [
|
|
136
|
+
# Note: in KùzuDB, some labels (e.g. `Macro`, `Property`, `Union`) are treated as reserved
|
|
137
|
+
# keywords in CREATE REL TABLE statements. We must escape them with backticks
|
|
138
|
+
# or the rel table creation will fail silently, leading to runtime
|
|
139
|
+
# "Binder exception: Table CONTAINS does not exist".
|
|
140
|
+
("CONTAINS", "FROM File TO Function, FROM File TO Class, FROM File TO Variable, FROM File TO Trait, FROM File TO Interface, FROM `Macro` TO `Macro`, FROM File TO `Macro`, FROM File TO Struct, FROM File TO Enum, FROM File TO `Union`, FROM File TO Annotation, FROM File TO Record, FROM File TO `Property`, FROM Repository TO Directory, FROM Directory TO Directory, FROM Directory TO File, FROM Repository TO File, FROM Class TO Function, FROM Function TO Function", True),
|
|
141
|
+
("CALLS", "FROM Function TO Function, FROM Function TO Class, FROM File TO Function, FROM File TO Class, FROM Class TO Function, FROM Class TO Class, line_number INT64, args STRING[], full_call_name STRING", True),
|
|
142
|
+
("IMPORTS", "FROM File TO Module, alias STRING, full_import_name STRING, imported_name STRING, line_number INT64", False),
|
|
143
|
+
("INHERITS", "FROM Class TO Class, FROM Record TO Record, FROM Interface TO Interface", True),
|
|
144
|
+
("HAS_PARAMETER", "FROM Function TO Parameter", False),
|
|
145
|
+
("INCLUDES", "FROM Class TO Module", False),
|
|
146
|
+
("IMPLEMENTS", "FROM Class TO Interface, FROM Struct TO Interface, FROM Record TO Interface", True)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for table_name, schema in node_tables:
|
|
150
|
+
try:
|
|
151
|
+
self._conn.execute(f"CREATE NODE TABLE `{table_name}`({schema})")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
if "already exists" not in str(e).lower():
|
|
154
|
+
warning_logger(f"Kuzu Schema Node Error ({table_name}): {e}")
|
|
155
|
+
debug_log(f"Kuzu Schema Node Error ({table_name}): {e}")
|
|
156
|
+
|
|
157
|
+
for table_name, schema, use_group in rel_tables:
|
|
158
|
+
try:
|
|
159
|
+
if use_group:
|
|
160
|
+
# KùzuDB requires CREATE REL TABLE GROUP for multi-binding relationships
|
|
161
|
+
self._conn.execute(f"CREATE REL TABLE GROUP `{table_name}`({schema})")
|
|
162
|
+
else:
|
|
163
|
+
self._conn.execute(f"CREATE REL TABLE `{table_name}`({schema})")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
if "already exists" not in str(e).lower():
|
|
166
|
+
warning_logger(f"Kuzu Schema Rel Error ({table_name}): {e}")
|
|
167
|
+
debug_log(f"Kuzu Schema Rel Error ({table_name}): {e}")
|
|
168
|
+
|
|
169
|
+
def close_driver(self):
|
|
170
|
+
"""Closes the connection."""
|
|
171
|
+
if self._conn is not None:
|
|
172
|
+
info_logger("Closing KùzuDB connection")
|
|
173
|
+
self._conn = None
|
|
174
|
+
self._db = None
|
|
175
|
+
|
|
176
|
+
def is_connected(self) -> bool:
|
|
177
|
+
"""Checks if the database connection is currently active."""
|
|
178
|
+
if self._conn is None:
|
|
179
|
+
return False
|
|
180
|
+
try:
|
|
181
|
+
self._conn.execute("RETURN 1")
|
|
182
|
+
return True
|
|
183
|
+
except Exception:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def get_backend_type(self) -> str:
|
|
187
|
+
"""Returns the database backend type."""
|
|
188
|
+
return 'kuzudb'
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def validate_config(db_path: str = None) -> Tuple[bool, Optional[str]]:
|
|
192
|
+
if db_path:
|
|
193
|
+
db_dir = Path(db_path).parent
|
|
194
|
+
if not os.access(db_dir, os.W_OK) and db_dir.exists():
|
|
195
|
+
return False, f"Cannot write to directory: {db_dir}"
|
|
196
|
+
return True, None
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def test_connection(db_path: str = None) -> Tuple[bool, Optional[str]]:
|
|
200
|
+
try:
|
|
201
|
+
import kuzu
|
|
202
|
+
return True, None
|
|
203
|
+
except ImportError:
|
|
204
|
+
return False, "KùzuDB is not installed. Run 'pip install kuzu'"
|
|
205
|
+
|
|
206
|
+
class KuzuDriverWrapper:
|
|
207
|
+
def __init__(self, conn):
|
|
208
|
+
self.conn = conn
|
|
209
|
+
def session(self):
|
|
210
|
+
return KuzuSessionWrapper(self.conn)
|
|
211
|
+
def close(self):
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
class KuzuSessionWrapper:
|
|
215
|
+
def __init__(self, conn):
|
|
216
|
+
self.conn = conn
|
|
217
|
+
self.uid_map = {
|
|
218
|
+
'Function': ['name', 'path', 'line_number'],
|
|
219
|
+
'Class': ['name', 'path', 'line_number'],
|
|
220
|
+
'Variable': ['name', 'path', 'line_number'],
|
|
221
|
+
'Trait': ['name', 'path', 'line_number'],
|
|
222
|
+
'Interface': ['name', 'path', 'line_number'],
|
|
223
|
+
'Macro': ['name', 'path', 'line_number'],
|
|
224
|
+
'Struct': ['name', 'path', 'line_number'],
|
|
225
|
+
'Enum': ['name', 'path', 'line_number'],
|
|
226
|
+
'Union': ['name', 'path', 'line_number'],
|
|
227
|
+
'Annotation': ['name', 'path', 'line_number'],
|
|
228
|
+
'Record': ['name', 'path', 'line_number'],
|
|
229
|
+
'Property': ['name', 'path', 'line_number'],
|
|
230
|
+
'Parameter': ['name', 'path', 'function_line_number']
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
def __enter__(self):
|
|
234
|
+
"""Enter context manager - return self for 'with' statement."""
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
238
|
+
"""Exit context manager - KùzuDB auto-commits, so nothing to do here."""
|
|
239
|
+
# KùzuDB uses auto-commit, no explicit commit needed
|
|
240
|
+
return False # Don't suppress exceptions
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _sanitize_value(v):
|
|
244
|
+
"""Recursively coerce Python types that KuzuDB cannot bind (tuples, sets, etc.)."""
|
|
245
|
+
if isinstance(v, tuple):
|
|
246
|
+
return [KuzuSessionWrapper._sanitize_value(i) for i in v]
|
|
247
|
+
if isinstance(v, set):
|
|
248
|
+
return [KuzuSessionWrapper._sanitize_value(i) for i in v]
|
|
249
|
+
if isinstance(v, list):
|
|
250
|
+
return [KuzuSessionWrapper._sanitize_value(i) for i in v]
|
|
251
|
+
if isinstance(v, dict):
|
|
252
|
+
return {k: KuzuSessionWrapper._sanitize_value(val) for k, val in v.items()}
|
|
253
|
+
return v
|
|
254
|
+
|
|
255
|
+
def run(self, query, **parameters):
|
|
256
|
+
# 0. Sanitize parameters (convert tuples/sets → lists throughout)
|
|
257
|
+
parameters = {k: self._sanitize_value(v) for k, v in parameters.items()}
|
|
258
|
+
# 1. Translate Query
|
|
259
|
+
debug_log(f"Original Query: {query[:200]}")
|
|
260
|
+
translated_query, translated_params = self._translate_query(query, parameters)
|
|
261
|
+
debug_log(f"Translated Query: {translated_query[:200]}")
|
|
262
|
+
try:
|
|
263
|
+
result = self.conn.execute(translated_query, translated_params)
|
|
264
|
+
return KuzuResultWrapper(result)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
# Silence specific non-errors
|
|
267
|
+
err_str = str(e).lower()
|
|
268
|
+
if "already exists" in err_str:
|
|
269
|
+
return KuzuResultWrapper(None)
|
|
270
|
+
error_logger(f"Kuzu Query failed: {query[:100]}... Error: {e}")
|
|
271
|
+
debug_log(f"Kuzu Query failed: {query[:100]}... Error: {e}")
|
|
272
|
+
raise
|
|
273
|
+
|
|
274
|
+
def _translate_query(self, query: str, parameters: Dict[str, Any]) -> Tuple[str, Dict[str, Any]]:
|
|
275
|
+
"""Translates Neo4j Cypher to Kuzu Cypher."""
|
|
276
|
+
|
|
277
|
+
# 0. Define Schema Map (Strict property filtering)
|
|
278
|
+
SCHEMA_MAP = {
|
|
279
|
+
'Repository': {'path', 'name', 'is_dependency'},
|
|
280
|
+
'File': {'path', 'name', 'relative_path', 'is_dependency'},
|
|
281
|
+
'Directory': {'path', 'name'},
|
|
282
|
+
'Module': {'name', 'lang', 'full_import_name'},
|
|
283
|
+
'Function': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'cyclomatic_complexity', 'context', 'context_type', 'class_context', 'is_dependency', 'decorators', 'args'},
|
|
284
|
+
'Class': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency', 'decorators'},
|
|
285
|
+
'Variable': {'uid', 'name', 'path', 'line_number', 'source', 'docstring', 'lang', 'value', 'context', 'is_dependency'},
|
|
286
|
+
'Trait': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
287
|
+
'Interface': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
288
|
+
'Macro': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
289
|
+
'Struct': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
290
|
+
'Enum': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
291
|
+
'Union': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
292
|
+
'Annotation': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
293
|
+
'Record': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
294
|
+
'Property': {'uid', 'name', 'path', 'line_number', 'end_line', 'source', 'docstring', 'lang', 'is_dependency'},
|
|
295
|
+
'Parameter': {'uid', 'name', 'path', 'function_line_number'}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# 1. Translate SET n += $props and SET n = $props (map merge/assign)
|
|
299
|
+
if "SET" in query and "= $" in query:
|
|
300
|
+
match = re.search(r'SET\s+(\w+)\s*\+?=\s*\$(\w+)', query)
|
|
301
|
+
if match:
|
|
302
|
+
node_var = match.group(1)
|
|
303
|
+
param_name = match.group(2)
|
|
304
|
+
|
|
305
|
+
# Determine label used for node_var to filter properties
|
|
306
|
+
def_match = re.search(rf'\({node_var}:(\w+)', query)
|
|
307
|
+
label = def_match.group(1) if def_match else None
|
|
308
|
+
|
|
309
|
+
props_dict = parameters.get(param_name, {})
|
|
310
|
+
if isinstance(props_dict, dict):
|
|
311
|
+
set_clauses = []
|
|
312
|
+
new_params = parameters.copy()
|
|
313
|
+
|
|
314
|
+
allowed_props = SCHEMA_MAP.get(label, set()) if label else None
|
|
315
|
+
|
|
316
|
+
for k, v in props_dict.items():
|
|
317
|
+
if isinstance(v, (dict, list)) and k != 'args' and k != 'decorators':
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
if allowed_props and k not in allowed_props:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
clean_k = f"{param_name}_{k}"
|
|
324
|
+
set_clauses.append(f"{node_var}.{k} = ${clean_k}")
|
|
325
|
+
new_params[clean_k] = v
|
|
326
|
+
|
|
327
|
+
if set_clauses:
|
|
328
|
+
query = query.replace(match.group(0), "SET " + ", ".join(set_clauses))
|
|
329
|
+
new_params.pop(param_name, None)
|
|
330
|
+
parameters = new_params
|
|
331
|
+
else:
|
|
332
|
+
query = query.replace(match.group(0), "")
|
|
333
|
+
|
|
334
|
+
# 1.5: Handle UNWIND-specific patterns before standard UID injection.
|
|
335
|
+
# When queries use UNWIND $batch AS row, two things need translation:
|
|
336
|
+
# a) SET n += row (map merge unsupported in KuzuDB) → explicit property SETs
|
|
337
|
+
# b) MERGE uid injection from row fields (row.name, row.line_number, …)
|
|
338
|
+
unwind_m = re.search(r'UNWIND\s+\$(\w+)\s+AS\s+(\w+)', query)
|
|
339
|
+
if unwind_m:
|
|
340
|
+
batch_param = unwind_m.group(1)
|
|
341
|
+
row_var = unwind_m.group(2)
|
|
342
|
+
batch_data = parameters.get(batch_param)
|
|
343
|
+
|
|
344
|
+
if isinstance(batch_data, list) and batch_data:
|
|
345
|
+
# 1.5a: Expand SET node_var += row_var → SET node_var.p1 = row_var.p1, …
|
|
346
|
+
set_plus_re = re.compile(
|
|
347
|
+
rf'SET\s+(\w+)\s*\+=\s*{re.escape(row_var)}\b'
|
|
348
|
+
)
|
|
349
|
+
set_m = set_plus_re.search(query)
|
|
350
|
+
if set_m:
|
|
351
|
+
node_var = set_m.group(1)
|
|
352
|
+
label_m = re.search(rf'\({re.escape(node_var)}:(\w+)', query)
|
|
353
|
+
label = label_m.group(1).strip('`') if label_m else None
|
|
354
|
+
allowed = SCHEMA_MAP.get(label, set()) if label else None
|
|
355
|
+
|
|
356
|
+
sample = batch_data[0]
|
|
357
|
+
parts = []
|
|
358
|
+
for k in sample:
|
|
359
|
+
if k == 'uid':
|
|
360
|
+
continue
|
|
361
|
+
if allowed and k not in allowed:
|
|
362
|
+
continue
|
|
363
|
+
parts.append(f"{node_var}.{k} = {row_var}.{k}")
|
|
364
|
+
|
|
365
|
+
replacement = ("SET " + ", ".join(parts)) if parts else ""
|
|
366
|
+
query = set_plus_re.sub(replacement, query, count=1)
|
|
367
|
+
|
|
368
|
+
# 1.5b: Inject uid into MERGE clauses that reference UNWIND row fields
|
|
369
|
+
merge_re = re.compile(
|
|
370
|
+
r'MERGE\s+\((\w+):([^\s\{]+)\s*\{([^}]+)\}\)'
|
|
371
|
+
)
|
|
372
|
+
for m in list(merge_re.finditer(query)):
|
|
373
|
+
var_name, label_raw, props_str = m.groups()
|
|
374
|
+
label = label_raw.strip('`')
|
|
375
|
+
if label not in self.uid_map:
|
|
376
|
+
continue
|
|
377
|
+
|
|
378
|
+
pk_parts = self.uid_map[label]
|
|
379
|
+
all_ok = True
|
|
380
|
+
|
|
381
|
+
for item in batch_data:
|
|
382
|
+
uid_components = []
|
|
383
|
+
for part in pk_parts:
|
|
384
|
+
row_ref = re.search(
|
|
385
|
+
rf'\b{part}\s*:\s*{re.escape(row_var)}\.(\w+)',
|
|
386
|
+
props_str,
|
|
387
|
+
)
|
|
388
|
+
param_ref = re.search(
|
|
389
|
+
rf'\b{part}\s*:\s*\$(\w+)', props_str
|
|
390
|
+
)
|
|
391
|
+
if row_ref:
|
|
392
|
+
val = item.get(row_ref.group(1))
|
|
393
|
+
if val is not None:
|
|
394
|
+
uid_components.append(str(val))
|
|
395
|
+
else:
|
|
396
|
+
all_ok = False
|
|
397
|
+
break
|
|
398
|
+
elif param_ref:
|
|
399
|
+
val = parameters.get(param_ref.group(1))
|
|
400
|
+
if val is not None:
|
|
401
|
+
uid_components.append(str(val))
|
|
402
|
+
else:
|
|
403
|
+
all_ok = False
|
|
404
|
+
break
|
|
405
|
+
else:
|
|
406
|
+
all_ok = False
|
|
407
|
+
break
|
|
408
|
+
|
|
409
|
+
if all_ok:
|
|
410
|
+
item['uid'] = ''.join(uid_components)
|
|
411
|
+
else:
|
|
412
|
+
all_ok = False
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
if all_ok:
|
|
416
|
+
old_block = '{' + props_str + '}'
|
|
417
|
+
new_block = (
|
|
418
|
+
'{' + props_str + f', uid: {row_var}.uid' + '}'
|
|
419
|
+
)
|
|
420
|
+
query = query.replace(old_block, new_block, 1)
|
|
421
|
+
|
|
422
|
+
# 1.5c: Strip explicit SET clauses for properties not in the schema
|
|
423
|
+
# (e.g. SET m.alias = row.alias when Module has no 'alias' column)
|
|
424
|
+
def _filter_set_clause(m_set):
|
|
425
|
+
full = m_set.group(0)
|
|
426
|
+
assignments = re.split(r',\s*(?=\w+\.\w+\s*=)', full[4:]) # skip "SET "
|
|
427
|
+
kept = []
|
|
428
|
+
for a in assignments:
|
|
429
|
+
a = a.strip()
|
|
430
|
+
prop_m = re.match(r'(\w+)\.(\w+)\s*=', a)
|
|
431
|
+
if prop_m:
|
|
432
|
+
nvar = prop_m.group(1)
|
|
433
|
+
prop_name = prop_m.group(2)
|
|
434
|
+
lbl_m = re.search(rf'\({re.escape(nvar)}:(\w+)', query)
|
|
435
|
+
if lbl_m:
|
|
436
|
+
lbl = lbl_m.group(1).strip('`')
|
|
437
|
+
allowed_s = SCHEMA_MAP.get(lbl)
|
|
438
|
+
if allowed_s and prop_name not in allowed_s:
|
|
439
|
+
continue
|
|
440
|
+
kept.append(a)
|
|
441
|
+
if kept:
|
|
442
|
+
return "SET " + ", ".join(kept)
|
|
443
|
+
return ""
|
|
444
|
+
|
|
445
|
+
# Only apply to explicit SET lines (not SET +=, already handled)
|
|
446
|
+
if '+=' not in query:
|
|
447
|
+
query = re.sub(
|
|
448
|
+
r'SET\s+\w+\.\w+\s*=\s*[^,\n]+(?:\s*,\s*\w+\.\w+\s*=\s*[^,\n]+)*',
|
|
449
|
+
_filter_set_clause,
|
|
450
|
+
query,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# 1.5d: Translate ON CREATE SET / ON MATCH SET → plain SET (KuzuDB compat)
|
|
454
|
+
query = re.sub(r'\bON\s+CREATE\s+SET\b', 'SET', query, flags=re.IGNORECASE)
|
|
455
|
+
query = re.sub(r'\bON\s+MATCH\s+SET\b', 'SET', query, flags=re.IGNORECASE)
|
|
456
|
+
|
|
457
|
+
# 2. Handle UID injection for MERGE (non-UNWIND queries)
|
|
458
|
+
# We look for MERGE (v:Label {props})
|
|
459
|
+
merge_pattern = r'MERGE\s+\((\w+):([^\s\{]+)\s*\{([^}]+)\}\)'
|
|
460
|
+
matches = list(re.finditer(merge_pattern, query))
|
|
461
|
+
for m in matches:
|
|
462
|
+
var_name, label_raw, props_str = m.groups()
|
|
463
|
+
label = label_raw.strip('`').strip(':')
|
|
464
|
+
if label in self.uid_map:
|
|
465
|
+
# Skip if uid already injected (by UNWIND handler above)
|
|
466
|
+
if 'uid:' in props_str:
|
|
467
|
+
continue
|
|
468
|
+
pk_parts = self.uid_map[label]
|
|
469
|
+
can_build_uid = True
|
|
470
|
+
uid_val = ""
|
|
471
|
+
for part in pk_parts:
|
|
472
|
+
p_match = re.search(rf'{part}:\s*\$(\w+)', props_str)
|
|
473
|
+
if p_match:
|
|
474
|
+
p_val = parameters.get(p_match.group(1))
|
|
475
|
+
if p_val is not None:
|
|
476
|
+
uid_val += str(p_val)
|
|
477
|
+
else: can_build_uid = False; break
|
|
478
|
+
else: can_build_uid = False; break
|
|
479
|
+
|
|
480
|
+
if can_build_uid:
|
|
481
|
+
uid_param = f"__uid_{var_name}"
|
|
482
|
+
old_block = f"{{{props_str}}}"
|
|
483
|
+
new_block = f"{{{props_str}, uid: ${uid_param}}}"
|
|
484
|
+
if old_block in query:
|
|
485
|
+
query = query.replace(old_block, new_block)
|
|
486
|
+
else:
|
|
487
|
+
warning_logger(f"Kuzu UID injection: could not find props block in query for label '{label}'")
|
|
488
|
+
|
|
489
|
+
parameters[uid_param] = uid_val
|
|
490
|
+
|
|
491
|
+
# 3. Escape keywords as labels
|
|
492
|
+
labels_to_escape = ['Macro', 'Union', 'Property', 'CONTAINS', 'CALLS'] # Only critical keywords
|
|
493
|
+
for label in labels_to_escape:
|
|
494
|
+
query = re.sub(rf':{label}\b', f':`{label}`', query)
|
|
495
|
+
|
|
496
|
+
# 4. Polymorphic matches and label access
|
|
497
|
+
query = query.replace("labels(n)[0]", "label(n)")
|
|
498
|
+
|
|
499
|
+
# Translate (n:Label1 OR n:Label2 ...) to label(n) IN ['Label1', 'Label2', ...]
|
|
500
|
+
def poly_replacer(match):
|
|
501
|
+
full_match = match.group(0)
|
|
502
|
+
var_name = match.group(1)
|
|
503
|
+
# Find all labels associated with this variable in the OR chain
|
|
504
|
+
labels = re.findall(rf'{var_name}:([a-zA-Z0-9_]+)', full_match)
|
|
505
|
+
return f"label({var_name}) IN {json.dumps(labels)}"
|
|
506
|
+
|
|
507
|
+
# Regex to match (n:Label1 OR n:Label2 OR n:Label3)
|
|
508
|
+
query = re.sub(r'\((\w+):[a-zA-Z0-9_]+(?:\s+OR\s+\1:[a-zA-Z0-9_]+)+\)', poly_replacer, query)
|
|
509
|
+
|
|
510
|
+
# Translate single WHERE n:Label to label(n) = 'Label'
|
|
511
|
+
# This is more complex because we don't want to match MATCH/MERGE
|
|
512
|
+
# For now, we only target where it appears after WHERE or AND/OR
|
|
513
|
+
def single_label_replacer(match):
|
|
514
|
+
prefix = match.group(1)
|
|
515
|
+
var_name = match.group(2)
|
|
516
|
+
label = match.group(3)
|
|
517
|
+
return f"{prefix}label({var_name}) = '{label}'"
|
|
518
|
+
|
|
519
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+|WHEN\s+)(\w+):([a-zA-Z0-9_]+)', single_label_replacer, query, flags=re.IGNORECASE)
|
|
520
|
+
|
|
521
|
+
# Handle NOT n:Label → NOT label(n) = 'Label'
|
|
522
|
+
def not_label_replacer(match):
|
|
523
|
+
prefix = match.group(1)
|
|
524
|
+
var_name = match.group(2)
|
|
525
|
+
label_name = match.group(3)
|
|
526
|
+
return f"{prefix}NOT label({var_name}) = '{label_name}'"
|
|
527
|
+
query = re.sub(r'(WHERE\s+|AND\s+|OR\s+)NOT\s+(\w+):([a-zA-Z0-9_]+)', not_label_replacer, query, flags=re.IGNORECASE)
|
|
528
|
+
|
|
529
|
+
query = query.replace("coalesce(", "COALESCE(")
|
|
530
|
+
query = re.sub(r'\btype\(', 'label(', query)
|
|
531
|
+
|
|
532
|
+
# General ON CREATE/MATCH SET → SET (also covers non-UNWIND queries)
|
|
533
|
+
query = re.sub(r'\bON\s+CREATE\s+SET\b', 'SET', query, flags=re.IGNORECASE)
|
|
534
|
+
query = re.sub(r'\bON\s+MATCH\s+SET\b', 'SET', query, flags=re.IGNORECASE)
|
|
535
|
+
|
|
536
|
+
if any(x in query.upper() for x in ["CREATE CONSTRAINT", "CREATE INDEX"]):
|
|
537
|
+
return "RETURN 1", {}
|
|
538
|
+
|
|
539
|
+
# 5. Cleanup unused parameters (Kuzu is strict)
|
|
540
|
+
used_params = set(re.findall(r'\$(\w+)', query))
|
|
541
|
+
parameters = {k: v for k, v in parameters.items() if k in used_params}
|
|
542
|
+
|
|
543
|
+
return query, parameters
|
|
544
|
+
|
|
545
|
+
def __enter__(self): return self
|
|
546
|
+
def __exit__(self, exc_type, exc_val, exc_tb): pass
|
|
547
|
+
|
|
548
|
+
class KuzuRecord:
|
|
549
|
+
def __init__(self, data_dict):
|
|
550
|
+
self._data = data_dict
|
|
551
|
+
self._keys = list(data_dict.keys())
|
|
552
|
+
|
|
553
|
+
def data(self):
|
|
554
|
+
return self._data
|
|
555
|
+
|
|
556
|
+
def keys(self):
|
|
557
|
+
return self._keys
|
|
558
|
+
|
|
559
|
+
def items(self):
|
|
560
|
+
return self._data.items()
|
|
561
|
+
|
|
562
|
+
def values(self):
|
|
563
|
+
return list(self._data.values())
|
|
564
|
+
|
|
565
|
+
def __len__(self):
|
|
566
|
+
return len(self._data)
|
|
567
|
+
|
|
568
|
+
def __getitem__(self, key):
|
|
569
|
+
# Support both dict-style (by name) and list-style (by index) access
|
|
570
|
+
if isinstance(key, int):
|
|
571
|
+
# Integer index - get by position
|
|
572
|
+
if 0 <= key < len(self._keys):
|
|
573
|
+
return self._data[self._keys[key]]
|
|
574
|
+
raise IndexError(f"Index {key} out of range")
|
|
575
|
+
else:
|
|
576
|
+
# String key - get by column name
|
|
577
|
+
return self._data[key]
|
|
578
|
+
|
|
579
|
+
def get(self, key, default=None):
|
|
580
|
+
return self._data.get(key, default)
|
|
581
|
+
|
|
582
|
+
class KuzuResultWrapper:
|
|
583
|
+
def __init__(self, result):
|
|
584
|
+
self.result = result
|
|
585
|
+
self._consumed = False
|
|
586
|
+
def consume(self):
|
|
587
|
+
self._consumed = True
|
|
588
|
+
return self
|
|
589
|
+
def single(self):
|
|
590
|
+
records = self.data_raw()
|
|
591
|
+
return KuzuRecord(records[0]) if records else None
|
|
592
|
+
def data_raw(self) -> List[Dict[str, Any]]:
|
|
593
|
+
if not self.result: return []
|
|
594
|
+
records = []
|
|
595
|
+
cols = self.result.get_column_names()
|
|
596
|
+
while self.result.has_next():
|
|
597
|
+
row = self.result.get_next()
|
|
598
|
+
record = {}
|
|
599
|
+
for i, val in enumerate(row):
|
|
600
|
+
# Handle Kuzu Node/Rel objects for visualization compatibility
|
|
601
|
+
processed_val = val
|
|
602
|
+
try:
|
|
603
|
+
# Kuzu 0.11+ objects often have a specific structure
|
|
604
|
+
if hasattr(val, '__class__') and 'Node' in str(val.__class__):
|
|
605
|
+
processed_val = val
|
|
606
|
+
if not hasattr(processed_val, 'labels'):
|
|
607
|
+
processed_val.labels = [val.get_label_name()]
|
|
608
|
+
if not hasattr(processed_val, 'id'):
|
|
609
|
+
props = val.get_properties()
|
|
610
|
+
processed_val.id = props.get('uid', props.get('path', str(id(val))))
|
|
611
|
+
if not hasattr(processed_val, 'properties'):
|
|
612
|
+
processed_val.properties = val.get_properties()
|
|
613
|
+
|
|
614
|
+
elif hasattr(val, '__class__') and 'Rel' in str(val.__class__):
|
|
615
|
+
processed_val = val
|
|
616
|
+
if not hasattr(processed_val, 'type'):
|
|
617
|
+
processed_val.type = val.get_label_name()
|
|
618
|
+
if not hasattr(processed_val, 'src_node'):
|
|
619
|
+
processed_val.src_node = val.get_src_id()['offset']
|
|
620
|
+
if not hasattr(processed_val, 'dest_node'):
|
|
621
|
+
processed_val.dest_node = val.get_dst_id()['offset']
|
|
622
|
+
if not hasattr(processed_val, 'properties'):
|
|
623
|
+
processed_val.properties = val.get_properties()
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
record[cols[i]] = processed_val
|
|
628
|
+
records.append(record)
|
|
629
|
+
return records
|
|
630
|
+
|
|
631
|
+
def data(self) -> List[Dict[str, Any]]:
|
|
632
|
+
# Return raw dict data, not KuzuRecord.data()
|
|
633
|
+
return self.data_raw()
|
|
634
|
+
|
|
635
|
+
def __iter__(self):
|
|
636
|
+
return iter([KuzuRecord(r) for r in self.data_raw()])
|