codegraphcontext 0.4.4__py3-none-any.whl → 0.4.6__py3-none-any.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 (41) hide show
  1. codegraphcontext/cli/cli_helpers.py +15 -3
  2. codegraphcontext/cli/config_manager.py +33 -3
  3. codegraphcontext/cli/main.py +2 -2
  4. codegraphcontext/cli/setup_wizard.py +20 -12
  5. codegraphcontext/core/__init__.py +28 -6
  6. codegraphcontext/core/cgc_bundle.py +3 -8
  7. codegraphcontext/core/database_falkordb.py +64 -19
  8. codegraphcontext/core/database_kuzu.py +54 -5
  9. codegraphcontext/core/database_nornic.py +204 -0
  10. codegraphcontext/core/watcher.py +92 -12
  11. codegraphcontext/server.py +116 -3
  12. codegraphcontext/tools/code_finder.py +122 -66
  13. codegraphcontext/tools/graph_builder.py +37 -7
  14. codegraphcontext/tools/handlers/analysis_handlers.py +54 -17
  15. codegraphcontext/tools/handlers/management_handlers.py +14 -3
  16. codegraphcontext/tools/handlers/query_handlers.py +14 -4
  17. codegraphcontext/tools/indexing/persistence/writer.py +37 -8
  18. codegraphcontext/tools/indexing/pre_scan.py +12 -2
  19. codegraphcontext/tools/indexing/resolution/calls.py +64 -5
  20. codegraphcontext/tools/indexing/schema.py +3 -3
  21. codegraphcontext/tools/languages/css.py +84 -0
  22. codegraphcontext/tools/languages/dart.py +110 -16
  23. codegraphcontext/tools/languages/html.py +123 -0
  24. codegraphcontext/tools/languages/java.py +58 -29
  25. codegraphcontext/tools/languages/javascript.py +38 -0
  26. codegraphcontext/tools/languages/lua.py +410 -0
  27. codegraphcontext/tools/languages/php.py +26 -18
  28. codegraphcontext/tools/languages/python.py +37 -1
  29. codegraphcontext/tools/languages/swift.py +51 -4
  30. codegraphcontext/tools/languages/typescript.py +1 -1
  31. codegraphcontext/tools/scip_indexer.py +4 -2
  32. codegraphcontext/tools/tree_sitter_parser.py +12 -0
  33. codegraphcontext/utils/git_utils.py +25 -0
  34. codegraphcontext/utils/tool_limits.py +85 -0
  35. codegraphcontext/utils/tree_sitter_manager.py +3 -0
  36. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/METADATA +62 -34
  37. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/RECORD +41 -35
  38. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/WHEEL +0 -0
  39. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/entry_points.txt +0 -0
  40. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/licenses/LICENSE +0 -0
  41. {codegraphcontext-0.4.4.dist-info → codegraphcontext-0.4.6.dist-info}/top_level.txt +0 -0
@@ -721,19 +721,31 @@ def watch_helper(path: str, context: Optional[str] = None):
721
721
  # Add the directory to watch
722
722
  if is_indexed:
723
723
  console.print("[green]✓[/green] Already indexed (no initial scan needed)")
724
- watcher.watch_directory(str(path_obj), perform_initial_scan=False)
724
+ watcher.watch_directory(
725
+ str(path_obj),
726
+ perform_initial_scan=False,
727
+ cgcignore_path=ctx.cgcignore_path,
728
+ )
725
729
  else:
726
730
  console.print("[yellow]⚠[/yellow] Not indexed yet. Performing initial scan...")
727
731
 
728
732
  # Index the repository first (like MCP does)
729
733
  async def do_index():
730
- await graph_builder.build_graph_from_path_async(path_obj, is_dependency=False)
734
+ await graph_builder.build_graph_from_path_async(
735
+ path_obj,
736
+ is_dependency=False,
737
+ cgcignore_path=ctx.cgcignore_path,
738
+ )
731
739
 
732
740
  asyncio.run(do_index())
733
741
  console.print("[green]✓[/green] Initial scan complete")
734
742
 
735
743
  # Now start watching (without another scan)
736
- watcher.watch_directory(str(path_obj), perform_initial_scan=False)
744
+ watcher.watch_directory(
745
+ str(path_obj),
746
+ perform_initial_scan=False,
747
+ cgcignore_path=ctx.cgcignore_path,
748
+ )
737
749
 
738
750
  console.print("[bold green]👀 Monitoring for file changes...[/bold green] (Press Ctrl+C to stop)")
739
751
  console.print("[dim]💡 Tip: Open a new terminal window to continue working[/dim]\n")
@@ -19,7 +19,10 @@ CONFIG_DIR = Path.home() / ".codegraphcontext"
19
19
  CONFIG_FILE = CONFIG_DIR / ".env"
20
20
 
21
21
  # Database credential keys (stored in same .env file but not managed as config)
22
- DATABASE_CREDENTIAL_KEYS = {"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE"}
22
+ DATABASE_CREDENTIAL_KEYS = {
23
+ "NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE",
24
+ "NORNIC_URI", "NORNIC_USERNAME", "NORNIC_PASSWORD", "NORNIC_DATABASE"
25
+ }
23
26
 
24
27
  # Default configuration values
25
28
  DEFAULT_CONFIG = {
@@ -47,11 +50,16 @@ DEFAULT_CONFIG = {
47
50
  "SCIP_INDEXER": "false",
48
51
  "SCIP_LANGUAGES": "python,typescript,go,rust,java",
49
52
  "SKIP_EXTERNAL_RESOLUTION": "false",
53
+ # 0 = unlimited; any positive integer caps MCP tool response size.
54
+ "MAX_TOOL_RESPONSE_TOKENS": "0",
55
+ # JSON object mapping tool names to integer result-count limits.
56
+ # Example: {"find_code": 20, "analyze_code_relationships": 10, "find_dead_code": 30}
57
+ "TOOL_RESULT_LIMITS": "{}",
50
58
  }
51
59
 
52
60
  # Configuration key descriptions
53
61
  CONFIG_DESCRIPTIONS = {
54
- "DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb)",
62
+ "DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb|nornic)",
55
63
  "FALKORDB_PATH": "Path to FalkorDB database file",
56
64
  "FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
57
65
  "INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
@@ -74,11 +82,13 @@ CONFIG_DESCRIPTIONS = {
74
82
  "SCIP_INDEXER": "Use SCIP-based indexing for higher accuracy call/inheritance resolution (requires scip-<lang> tools installed)",
75
83
  "SCIP_LANGUAGES": "Comma-separated languages to index via SCIP when SCIP_INDEXER=true (python,typescript,go,rust,java)",
76
84
  "SKIP_EXTERNAL_RESOLUTION": "Skip resolution attempts for external library method calls (recommended for enterprise large Java/Spring codebases)",
85
+ "MAX_TOOL_RESPONSE_TOKENS": "Maximum tokens per MCP tool response (0 = unlimited). Truncates oversized payloads and appends a notice.",
86
+ "TOOL_RESULT_LIMITS": "JSON object mapping tool names to max result counts, e.g. {\"find_code\": 20, \"analyze_code_relationships\": 10}. Missing keys use built-in defaults.",
77
87
  }
78
88
 
79
89
  # Valid values for each config key
80
90
  CONFIG_VALIDATORS = {
81
- "DEFAULT_DATABASE": ["neo4j", "falkordb", "falkordb-remote", "kuzudb"],
91
+ "DEFAULT_DATABASE": ["neo4j", "falkordb", "falkordb-remote", "kuzudb", "nornic"],
82
92
  "INDEX_VARIABLES": ["true", "false"],
83
93
  "ALLOW_DB_DELETION": ["true", "false"],
84
94
  "DEBUG_LOGS": ["true", "false"],
@@ -342,6 +352,26 @@ def validate_config_value(key: str, value: str) -> tuple[bool, Optional[str]]:
342
352
  return False, "PARALLEL_WORKERS must be between 1 and 32"
343
353
  except ValueError:
344
354
  return False, "PARALLEL_WORKERS must be a number"
355
+
356
+ if key == "MAX_TOOL_RESPONSE_TOKENS":
357
+ try:
358
+ limit = int(value)
359
+ if limit < 0:
360
+ return False, "MAX_TOOL_RESPONSE_TOKENS must be 0 (unlimited) or a positive integer"
361
+ except ValueError:
362
+ return False, "MAX_TOOL_RESPONSE_TOKENS must be an integer (0 = unlimited)"
363
+
364
+ if key == "TOOL_RESULT_LIMITS":
365
+ import json as _json
366
+ try:
367
+ parsed = _json.loads(value)
368
+ if not isinstance(parsed, dict):
369
+ return False, "TOOL_RESULT_LIMITS must be a JSON object, e.g. {\"find_code\": 20}"
370
+ for k, v in parsed.items():
371
+ if not isinstance(v, int) or v < 1:
372
+ return False, f"TOOL_RESULT_LIMITS: value for '{k}' must be a positive integer"
373
+ except _json.JSONDecodeError:
374
+ return False, "TOOL_RESULT_LIMITS must be valid JSON, e.g. {\"find_code\": 20, \"find_dead_code\": 30}"
345
375
 
346
376
  if key == "MAX_DEPTH":
347
377
  if value.lower() != "unlimited":
@@ -854,11 +854,11 @@ def doctor():
854
854
  elif default_db == "kuzudb":
855
855
  from importlib.util import find_spec
856
856
 
857
- if find_spec("kuzu") is not None:
857
+ if find_spec("real_ladybug") is not None:
858
858
  console.print(f" [green]✓[/green] KuzuDB is installed")
859
859
  else:
860
860
  console.print(f" [red]✗[/red] KuzuDB is not installed")
861
- console.print(f" Run: pip install kuzu")
861
+ console.print(f" Run: pip install real_ladybug")
862
862
  all_checks_passed = False
863
863
  else:
864
864
  # FalkorDB
@@ -54,15 +54,18 @@ def _save_neo4j_credentials(creds):
54
54
 
55
55
  def _generate_mcp_json(creds):
56
56
  """Generates and prints the MCP JSON configuration."""
57
- cgc_path = shutil.which("cgc") or sys.executable
57
+ cgc_path = shutil.which("cgc")
58
+ pipx_path = shutil.which("pipx")
58
59
 
59
- if "python" in Path(cgc_path).name:
60
- # fallback to running as module if no cgc binary is found
60
+ if cgc_path:
61
61
  command = cgc_path
62
- args = ["-m", "cgc", "mcp", "start"]
62
+ args = ["mcp", "start"]
63
+ elif pipx_path:
64
+ command = pipx_path
65
+ args = ["run", "codegraphcontext", "mcp", "start"]
63
66
  else:
64
- command = cgc_path
65
- args = ["mcp","start"]
67
+ command = sys.executable
68
+ args = ["-m", "codegraphcontext", "mcp", "start"]
66
69
 
67
70
  mcp_config = {
68
71
  "mcpServers": {
@@ -84,6 +87,7 @@ def _generate_mcp_json(creds):
84
87
  "list_indexed_repositories", "delete_repository", "list_watched_paths",
85
88
  "unwatch_directory", "visualize_graph_query"
86
89
  ],
90
+ "disabledTools": [],
87
91
  "disabled": False
88
92
  },
89
93
  "disabled": False,
@@ -424,15 +428,18 @@ def configure_mcp_client():
424
428
  env_vars[key] = value
425
429
 
426
430
  # Generate MCP configuration
427
- cgc_path = shutil.which("cgc") or sys.executable
431
+ cgc_path = shutil.which("cgc")
432
+ pipx_path = shutil.which("pipx")
428
433
 
429
- if "python" in Path(cgc_path).name:
430
- # fallback to running as module if no cgc binary is found
431
- command = cgc_path
432
- args = ["-m", "cgc", "mcp", "start"]
433
- else:
434
+ if cgc_path:
434
435
  command = cgc_path
435
436
  args = ["mcp", "start"]
437
+ elif pipx_path:
438
+ command = pipx_path
439
+ args = ["run", "codegraphcontext", "mcp", "start"]
440
+ else:
441
+ command = sys.executable
442
+ args = ["-m", "codegraphcontext", "mcp", "start"]
436
443
 
437
444
  # Create MCP config with complete env section
438
445
  mcp_config = {
@@ -451,6 +458,7 @@ def configure_mcp_client():
451
458
  "list_indexed_repositories", "delete_repository", "list_watched_paths",
452
459
  "unwatch_directory", "visualize_graph_query"
453
460
  ],
461
+ "disabledTools": [],
454
462
  "disabled": False
455
463
  },
456
464
  "disabled": False,
@@ -22,7 +22,7 @@ import importlib.util
22
22
  def _is_kuzudb_available() -> bool:
23
23
  """Check if KùzuDB is installed."""
24
24
  try:
25
- return importlib.util.find_spec("kuzu") is not None
25
+ return importlib.util.find_spec("real_ladybug") is not None
26
26
  except ImportError:
27
27
  return False
28
28
 
@@ -52,7 +52,15 @@ def _is_neo4j_configured() -> bool:
52
52
  os.getenv('NEO4J_PASSWORD')
53
53
  ])
54
54
 
55
- def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager']:
55
+ def _is_nornic_configured() -> bool:
56
+ """Check if Nornic is configured with credentials."""
57
+ return all([
58
+ os.getenv('NORNIC_URI'),
59
+ os.getenv('NORNIC_USERNAME'),
60
+ os.getenv('NORNIC_PASSWORD')
61
+ ])
62
+
63
+ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'NornicDBManager']:
56
64
  """
57
65
  Factory function to get the appropriate database manager based on configuration.
58
66
 
@@ -70,7 +78,7 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
70
78
  db_type = db_type.lower()
71
79
  if db_type == 'kuzudb':
72
80
  if not _is_kuzudb_available():
73
- raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install kuzu'")
81
+ raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install real_ladybug'")
74
82
  from .database_kuzu import KuzuDBManager
75
83
  info_logger(f"Using KùzuDB (explicit) at {db_path or 'default path'}")
76
84
  return KuzuDBManager(db_path=db_path)
@@ -111,8 +119,15 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
111
119
  from .database import DatabaseManager
112
120
  info_logger("Using Neo4j Server (explicit)")
113
121
  return DatabaseManager()
122
+
123
+ elif db_type == 'nornic':
124
+ if not _is_nornic_configured():
125
+ raise ValueError("Database set to 'nornic' but it is not configured.")
126
+ from .database_nornic import NornicDBManager
127
+ info_logger("Using Nornic DB (explicit)")
128
+ return NornicDBManager()
114
129
  else:
115
- raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', or 'neo4j'.")
130
+ raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', 'neo4j', or 'nornic'.")
116
131
 
117
132
  # Implicit: remote FalkorDB when FALKORDB_HOST is set (explicit infra signal)
118
133
  if _is_falkordb_remote_configured():
@@ -146,8 +161,14 @@ def get_database_manager(db_path: Optional[str] = None) -> Union['DatabaseManage
146
161
  info_logger("Using Neo4j Server (auto-detected)")
147
162
  return DatabaseManager()
148
163
 
164
+ # Implicit: Nornic when configured
165
+ if _is_nornic_configured():
166
+ from .database_nornic import NornicDBManager
167
+ info_logger("Using Nornic DB (auto-detected)")
168
+ return NornicDBManager()
169
+
149
170
  error_msg = "No database backend available.\n"
150
- error_msg += "Recommended: Install KùzuDB for zero-config ('pip install kuzu')\n"
171
+ error_msg += "Recommended: Install KùzuDB for zero-config ('pip install real_ladybug')\n"
151
172
 
152
173
  if platform.system() != "Windows":
153
174
  error_msg += "Alternative: Install FalkorDB Lite ('pip install falkordblite')\n"
@@ -161,5 +182,6 @@ from .database import DatabaseManager
161
182
  from .database_falkordb import FalkorDBManager
162
183
  from .database_falkordb_remote import FalkorDBRemoteManager
163
184
  from .database_kuzu import KuzuDBManager
185
+ from .database_nornic import NornicDBManager
164
186
 
165
- __all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'get_database_manager']
187
+ __all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'NornicDBManager', 'get_database_manager']
@@ -28,6 +28,7 @@ from datetime import datetime, date
28
28
  import subprocess
29
29
 
30
30
  from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
31
+ from codegraphcontext.utils.git_utils import get_repo_commit_hash
31
32
 
32
33
 
33
34
  class _BundleEncoder(json.JSONEncoder):
@@ -293,15 +294,9 @@ class CGCBundle:
293
294
 
294
295
  # Try to get git information if available
295
296
  if repo_path and repo_path.exists():
296
- try:
297
- commit = subprocess.check_output(
298
- ['git', 'rev-parse', 'HEAD'],
299
- cwd=repo_path,
300
- stderr=subprocess.DEVNULL
301
- ).decode().strip()
297
+ commit = get_repo_commit_hash(repo_path)
298
+ if commit:
302
299
  metadata["commit"] = commit[:8]
303
- except (subprocess.CalledProcessError, FileNotFoundError):
304
- pass
305
300
 
306
301
  try:
307
302
  result = session.run("""
@@ -356,6 +356,18 @@ class FalkorDBSessionWrapper:
356
356
  """
357
357
  Execute a Cypher query on FalkorDB.
358
358
  """
359
+ constraint_command = self._translate_constraint_command(query)
360
+ if constraint_command is not None:
361
+ try:
362
+ self.graph.execute_command(*constraint_command)
363
+ return FalkorDBResultWrapper(None)
364
+ except Exception as e:
365
+ error_msg = str(e).lower()
366
+ if "already exists" in error_msg or "already created" in error_msg:
367
+ return FalkorDBResultWrapper(None)
368
+ error_logger(f"FalkorDB constraint failed: {constraint_command!r} Error: {e}")
369
+ raise
370
+
359
371
  # Translate Neo4j schema queries to FalkorDB syntax
360
372
  query = self._translate_schema_query(query)
361
373
 
@@ -371,6 +383,56 @@ class FalkorDBSessionWrapper:
371
383
  error_logger(f"FalkorDB query failed: {query[:100]}... Error: {e}")
372
384
  raise
373
385
 
386
+ def _translate_constraint_command(self, query: str):
387
+ """
388
+ Translate Neo4j-style CREATE CONSTRAINT queries to GRAPH.CONSTRAINT CREATE.
389
+ FalkorDB 4.16.x expects this command path instead of GRAPH.QUERY.
390
+ """
391
+ q_upper = query.upper()
392
+ if "CREATE CONSTRAINT" not in q_upper:
393
+ return None
394
+
395
+ normalized = re.sub(r"\s+IF NOT EXISTS", "", query, flags=re.IGNORECASE)
396
+ normalized = re.sub(r"\s+", " ", normalized).strip()
397
+
398
+ entity_match = re.search(r"FOR\s*\((\w+):([^)]+)\)", normalized, flags=re.IGNORECASE)
399
+ if not entity_match:
400
+ return None
401
+ entity_type = "NODE"
402
+ label = entity_match.group(2).strip()
403
+
404
+ composite_match = re.search(
405
+ r"REQUIRE\s*\(([^)]+)\)\s*IS\s+UNIQUE",
406
+ normalized,
407
+ flags=re.IGNORECASE,
408
+ )
409
+ single_match = re.search(
410
+ r"REQUIRE\s+\w+\.([A-Za-z_][A-Za-z0-9_]*)\s+IS\s+UNIQUE",
411
+ normalized,
412
+ flags=re.IGNORECASE,
413
+ )
414
+
415
+ if composite_match:
416
+ props = [part.split(".")[-1].strip() for part in composite_match.group(1).split(",") if part.strip()]
417
+ constraint_type = "UNIQUE"
418
+ elif single_match:
419
+ props = [single_match.group(1).strip()]
420
+ constraint_type = "UNIQUE"
421
+ else:
422
+ return None
423
+
424
+ return [
425
+ "GRAPH.CONSTRAINT",
426
+ "CREATE",
427
+ self.graph.name,
428
+ constraint_type,
429
+ entity_type,
430
+ label,
431
+ "PROPERTIES",
432
+ len(props),
433
+ *props,
434
+ ]
435
+
374
436
  def _translate_schema_query(self, query: str) -> str:
375
437
  """Translate Neo4j schema queries to FalkorDB/RedisGraph syntax."""
376
438
  q_upper = query.upper()
@@ -379,26 +441,9 @@ class FalkorDBSessionWrapper:
379
441
  if "CREATE FULLTEXT INDEX" in q_upper:
380
442
  return "RETURN 1"
381
443
 
382
- # Handle Constraints
444
+ # Handle Constraints through GRAPH.CONSTRAINT in run()
383
445
  if "CREATE CONSTRAINT" in q_upper:
384
- # Remove "IF NOT EXISTS"
385
- query = re.sub(r'\s+IF NOT EXISTS', '', query, flags=re.IGNORECASE)
386
-
387
- # Handle composite keys: (n.p1, n.p2) -> downgrade to INDEX
388
- if "," in query:
389
- match_node = re.search(r'FOR\s+(\([^)]+\))', query, flags=re.IGNORECASE)
390
- match_props = re.search(r'REQUIRE\s+(\([^)]+\))\s+IS UNIQUE', query, flags=re.IGNORECASE)
391
-
392
- if match_node and match_props:
393
- return f"CREATE INDEX FOR {match_node.group(1)} ON {match_props.group(1)}"
394
-
395
- # Handle simple uniqueness: CREATE CONSTRAINT name FOR (n:Label) REQUIRE n.prop IS UNIQUE
396
- # TO: CREATE CONSTRAINT ON (n:Label) ASSERT n.prop IS UNIQUE
397
-
398
- # Remove constraint name
399
- query = re.sub(r'CREATE CONSTRAINT\s+\w+\s+', 'CREATE CONSTRAINT ', query, flags=re.IGNORECASE)
400
- query = re.sub(r'\s+FOR\s+', ' ON ', query, flags=re.IGNORECASE)
401
- query = re.sub(r'\s+REQUIRE\s+', ' ASSERT ', query, flags=re.IGNORECASE)
446
+ return "RETURN 1"
402
447
 
403
448
  # Handle Regular Indexes
404
449
  elif "CREATE INDEX" in q_upper:
@@ -66,7 +66,7 @@ class KuzuDBManager:
66
66
  if self._conn is None:
67
67
  with self._lock:
68
68
  if self._conn is None:
69
- import kuzu
69
+ import real_ladybug as kuzu
70
70
  max_retries = 5
71
71
  for attempt in range(max_retries):
72
72
  try:
@@ -77,7 +77,7 @@ class KuzuDBManager:
77
77
  info_logger("KùzuDB connection established and schema verified")
78
78
  break
79
79
  except ImportError:
80
- error_logger("KùzuDB is not installed. Run 'pip install kuzu'")
80
+ error_logger("KùzuDB is not installed. Run 'pip install real_ladybug'")
81
81
  raise ValueError("KùzuDB missing.")
82
82
  except Exception as e:
83
83
  if "lock" in str(e).lower() and attempt < max_retries - 1:
@@ -99,7 +99,7 @@ class KuzuDBManager:
99
99
  # but we can wrap in try-except or check metadata.
100
100
 
101
101
  node_tables = [
102
- ("Repository", "path STRING, name STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
102
+ ("Repository", "path STRING, name STRING, is_dependency BOOLEAN, indexed_at STRING, commit_hash STRING, PRIMARY KEY (path)"),
103
103
  ("File", "path STRING, name STRING, relative_path STRING, is_dependency BOOLEAN, PRIMARY KEY (path)"),
104
104
  ("Directory", "path STRING, name STRING, PRIMARY KEY (path)"),
105
105
  ("Module", "name STRING, lang STRING, full_import_name STRING, PRIMARY KEY (name)"),
@@ -156,6 +156,29 @@ class KuzuDBManager:
156
156
  warning_logger(f"Kuzu Schema Rel Error ({table_name}): {e}")
157
157
  debug_log(f"Kuzu Schema Rel Error ({table_name}): {e}")
158
158
 
159
+ self._run_schema_migrations()
160
+
161
+ def _run_schema_migrations(self):
162
+ """Add columns introduced after older local Kùzu databases were created."""
163
+ migrations = [
164
+ ("Module", "full_import_name", "STRING"),
165
+ ("IMPORTS", "full_import_name", "STRING"),
166
+ ("IMPORTS", "imported_name", "STRING"),
167
+ # Freshness properties added to Repository in 0.4.6
168
+ ("Repository", "indexed_at", "STRING"),
169
+ ("Repository", "commit_hash", "STRING"),
170
+ ]
171
+
172
+ for table_name, column_name, column_type in migrations:
173
+ try:
174
+ self._conn.execute(f"ALTER TABLE `{table_name}` ADD {column_name} {column_type}")
175
+ except Exception as e:
176
+ err = str(e).lower()
177
+ if "already exists" in err or "duplicate" in err or "already has property" in err:
178
+ continue
179
+ warning_logger(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
180
+ debug_log(f"Kuzu Schema Migration Error ({table_name}.{column_name}): {e}")
181
+
159
182
  def close_driver(self):
160
183
  """Closes the connection."""
161
184
  if self._conn is not None:
@@ -188,10 +211,10 @@ class KuzuDBManager:
188
211
  @staticmethod
189
212
  def test_connection(db_path: str = None) -> Tuple[bool, Optional[str]]:
190
213
  try:
191
- import kuzu
214
+ import real_ladybug as kuzu
192
215
  return True, None
193
216
  except ImportError:
194
- return False, "KùzuDB is not installed. Run 'pip install kuzu'"
217
+ return False, "KùzuDB is not installed. Run 'pip install real_ladybug'"
195
218
 
196
219
  class KuzuDriverWrapper:
197
220
  def __init__(self, conn):
@@ -257,6 +280,32 @@ class KuzuSessionWrapper:
257
280
  err_str = str(e).lower()
258
281
  if "already exists" in err_str:
259
282
  return KuzuResultWrapper(None)
283
+
284
+ # Fallback for KuzuDB UNWIND bug (unordered_map::at)
285
+ if "unordered_map::at" in err_str and "UNWIND" in query:
286
+ unwind_m = re.search(r'UNWIND\s+\$(\w+)\s+AS\s+(\w+)', query)
287
+ if unwind_m:
288
+ batch_param = unwind_m.group(1)
289
+ row_var = unwind_m.group(2)
290
+ batch_data = parameters.get(batch_param)
291
+ if isinstance(batch_data, list):
292
+ loop_query = re.sub(r'UNWIND\s+\$\w+\s+AS\s+\w+', '', query, count=1)
293
+ # Find all row.prop usages and replace with $row_prop
294
+ props_used = set(re.findall(rf'{row_var}\.(\w+)', loop_query))
295
+ for p in props_used:
296
+ loop_query = loop_query.replace(f"{row_var}.{p}", f"${row_var}_{p}")
297
+
298
+ last_result = None
299
+ for item in batch_data:
300
+ loop_params = parameters.copy()
301
+ loop_params.pop(batch_param, None)
302
+ for p in props_used:
303
+ loop_params[f"{row_var}_{p}"] = item.get(p)
304
+ if "uid" in item:
305
+ loop_params[f"{row_var}_uid"] = item["uid"]
306
+ last_result = self.run(loop_query, **loop_params)
307
+ return last_result or KuzuResultWrapper(None)
308
+
260
309
  error_logger(f"Kuzu Query failed: {query[:100]}... Error: {e}")
261
310
  debug_log(f"Kuzu Query failed: {query[:100]}... Error: {e}")
262
311
  raise