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.
Files changed (101) hide show
  1. codegraphcontext/__init__.py +2 -0
  2. codegraphcontext/__main__.py +5 -0
  3. codegraphcontext/_cgc_rust.cp310-win_amd64.pyd +0 -0
  4. codegraphcontext/cli/__init__.py +1 -0
  5. codegraphcontext/cli/cli_helpers.py +774 -0
  6. codegraphcontext/cli/config_manager.py +1051 -0
  7. codegraphcontext/cli/main.py +2386 -0
  8. codegraphcontext/cli/registry_commands.py +404 -0
  9. codegraphcontext/cli/setup_macos.py +93 -0
  10. codegraphcontext/cli/setup_wizard.py +991 -0
  11. codegraphcontext/cli/visualizer.py +50 -0
  12. codegraphcontext/core/__init__.py +165 -0
  13. codegraphcontext/core/bundle_registry.py +181 -0
  14. codegraphcontext/core/cgc_bundle.py +858 -0
  15. codegraphcontext/core/cgcignore.py +118 -0
  16. codegraphcontext/core/database.py +273 -0
  17. codegraphcontext/core/database_falkordb.py +480 -0
  18. codegraphcontext/core/database_falkordb_remote.py +199 -0
  19. codegraphcontext/core/database_kuzu.py +636 -0
  20. codegraphcontext/core/falkor_worker.py +132 -0
  21. codegraphcontext/core/jobs.py +132 -0
  22. codegraphcontext/core/watcher.py +260 -0
  23. codegraphcontext/prompts.py +125 -0
  24. codegraphcontext/server.py +471 -0
  25. codegraphcontext/tool_definitions.py +220 -0
  26. codegraphcontext/tools/__init__.py +0 -0
  27. codegraphcontext/tools/advanced_language_query_tool.py +103 -0
  28. codegraphcontext/tools/code_finder.py +1160 -0
  29. codegraphcontext/tools/graph_builder.py +337 -0
  30. codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
  31. codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
  32. codegraphcontext/tools/handlers/management_handlers.py +339 -0
  33. codegraphcontext/tools/handlers/query_handlers.py +83 -0
  34. codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
  35. codegraphcontext/tools/indexing/__init__.py +1 -0
  36. codegraphcontext/tools/indexing/constants.py +25 -0
  37. codegraphcontext/tools/indexing/discovery.py +64 -0
  38. codegraphcontext/tools/indexing/engine.py +109 -0
  39. codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
  40. codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
  41. codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
  42. codegraphcontext/tools/indexing/pipeline.py +171 -0
  43. codegraphcontext/tools/indexing/pre_scan.py +105 -0
  44. codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
  45. codegraphcontext/tools/indexing/resolution/calls.py +253 -0
  46. codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
  47. codegraphcontext/tools/indexing/sanitize.py +41 -0
  48. codegraphcontext/tools/indexing/schema.py +79 -0
  49. codegraphcontext/tools/indexing/schema_contract.py +44 -0
  50. codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
  51. codegraphcontext/tools/languages/c.py +563 -0
  52. codegraphcontext/tools/languages/cpp.py +616 -0
  53. codegraphcontext/tools/languages/csharp.py +551 -0
  54. codegraphcontext/tools/languages/dart.py +378 -0
  55. codegraphcontext/tools/languages/elixir.py +460 -0
  56. codegraphcontext/tools/languages/go.py +499 -0
  57. codegraphcontext/tools/languages/haskell.py +426 -0
  58. codegraphcontext/tools/languages/java.py +500 -0
  59. codegraphcontext/tools/languages/javascript.py +589 -0
  60. codegraphcontext/tools/languages/kotlin.py +640 -0
  61. codegraphcontext/tools/languages/perl.py +260 -0
  62. codegraphcontext/tools/languages/php.py +519 -0
  63. codegraphcontext/tools/languages/python.py +576 -0
  64. codegraphcontext/tools/languages/ruby.py +537 -0
  65. codegraphcontext/tools/languages/rust.py +295 -0
  66. codegraphcontext/tools/languages/scala.py +519 -0
  67. codegraphcontext/tools/languages/swift.py +491 -0
  68. codegraphcontext/tools/languages/typescript.py +575 -0
  69. codegraphcontext/tools/languages/typescriptjsx.py +151 -0
  70. codegraphcontext/tools/package_resolver.py +472 -0
  71. codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
  72. codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
  73. codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
  74. codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
  75. codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
  76. codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
  77. codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
  78. codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
  79. codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
  80. codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
  81. codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
  82. codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
  83. codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
  84. codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
  85. codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
  86. codegraphcontext/tools/scip_indexer.py +467 -0
  87. codegraphcontext/tools/scip_pb2.py +2455 -0
  88. codegraphcontext/tools/system.py +133 -0
  89. codegraphcontext/tools/tree_sitter_parser.py +103 -0
  90. codegraphcontext/utils/debug_log.py +90 -0
  91. codegraphcontext/utils/path_ignore.py +55 -0
  92. codegraphcontext/utils/repo_path.py +27 -0
  93. codegraphcontext/utils/tree_sitter_manager.py +285 -0
  94. codegraphcontext/utils/visualize_graph.py +0 -0
  95. codegraphcontext/viz/server.py +282 -0
  96. codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
  97. codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
  98. codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
  99. codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
  100. codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
  101. 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()])