codegraphcontext 0.2.8__py3-none-any.whl → 0.2.9__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.
@@ -1,6 +1,6 @@
1
- # src/codegraphcontext/cli/cli_helpers.py
2
1
  import asyncio
3
2
  import json
3
+ import uuid
4
4
  import urllib.parse
5
5
  from pathlib import Path
6
6
  import time
@@ -27,10 +27,32 @@ def _initialize_services():
27
27
 
28
28
  try:
29
29
  db_manager.get_driver()
30
- except ValueError as e:
31
- console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
32
- console.print("Please ensure your Neo4j credentials are correct and the database is running.")
33
- return None, None, None
30
+ except Exception as e:
31
+ # Check if this is a FalkorDB failure that should trigger a KùzuDB fallback
32
+ from ..core.database_falkordb import FalkorDBUnavailableError
33
+ if isinstance(e, FalkorDBUnavailableError):
34
+ console.print(f"[yellow]⚠ FalkorDB Lite is not functional in this environment: {e}[/yellow]")
35
+ console.print("[cyan]Falling back to KùzuDB for a reliable experience...[/cyan]")
36
+
37
+ # Close the broken driver/socket
38
+ try:
39
+ db_manager.close_driver()
40
+ except Exception:
41
+ pass
42
+
43
+ # Re-initialize explicitly with KùzuDB
44
+ from ..core.database_kuzu import KuzuDBManager
45
+ db_manager = KuzuDBManager()
46
+ try:
47
+ db_manager.get_driver()
48
+ console.print("[green]✓[/green] Successfully switched to KùzuDB fallback")
49
+ except Exception as kuzu_e:
50
+ console.print(f"[bold red]Critical Error:[/bold red] Both FalkorDB and KùzuDB failed: {kuzu_e}")
51
+ return None, None, None
52
+ else:
53
+ console.print(f"[bold red]Database Connection Error:[/bold red] {e}")
54
+ console.print("Please ensure your database is configured correctly or run 'cgc doctor'.")
55
+ return None, None, None
34
56
 
35
57
  # The GraphBuilder requires an event loop, even for synchronous-style execution
36
58
  try:
@@ -263,16 +285,28 @@ def cypher_helper_visual(query: str):
263
285
  import webbrowser
264
286
 
265
287
  def visualize_helper(query: str):
266
- """Generates a visualization."""
288
+ """"Generates a visualization."""
267
289
  services = _initialize_services()
268
290
  if not all(services):
269
291
  return
270
292
 
271
293
  db_manager, _, _ = services
272
294
 
273
- # Check if FalkorDB
274
- if "FalkorDB" in db_manager.__class__.__name__:
295
+ # Check Backend Type
296
+ backend = getattr(db_manager, "name", "").lower()
297
+ if not backend:
298
+ # Fallback check
299
+ if "FalkorDB" in db_manager.__class__.__name__:
300
+ backend = "falkordb"
301
+ elif "Kuzu" in db_manager.__class__.__name__:
302
+ backend = "kuzudb"
303
+ else:
304
+ backend = "neo4j"
305
+
306
+ if backend == "falkordb":
275
307
  _visualize_falkordb(db_manager)
308
+ elif backend == "kuzudb":
309
+ _visualize_kuzudb(db_manager)
276
310
  else:
277
311
  try:
278
312
  encoded_query = urllib.parse.quote(query)
@@ -391,6 +425,138 @@ def _visualize_falkordb(db_manager):
391
425
  db_manager.close_driver()
392
426
 
393
427
 
428
+ def _visualize_kuzudb(db_manager):
429
+ console.print("[dim]Generating KùzuDB visualization (showing up to 500 relationships)...[/dim]")
430
+ try:
431
+ data_nodes = []
432
+ data_edges = []
433
+
434
+ with db_manager.get_driver().session() as session:
435
+ # Fetch nodes and edges
436
+ # KùzuDB returns dicts for n, r, m in the result
437
+ q = "MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 500"
438
+ result = session.run(q)
439
+
440
+ seen_nodes = set()
441
+
442
+ # Helper to extract Node ID and props
443
+ def process_node(node):
444
+ uid = None
445
+ lbl = 'Node'
446
+ props = {}
447
+
448
+ # Handle Kuzu Node Object (processed by wrapper)
449
+ if hasattr(node, 'properties'):
450
+ props = node.properties or {}
451
+ if hasattr(node, 'labels') and node.labels:
452
+ lbl = node.labels[0]
453
+ if hasattr(node, 'id'):
454
+ uid = str(node.id)
455
+ # Handle Dictionary (raw Kuzu result)
456
+ elif isinstance(node, dict):
457
+ if '_id' in node:
458
+ uid = f"{node['_id']['table']}_{node['_id']['offset']}"
459
+ lbl = node.get('_label', 'Node')
460
+ props = {k: v for k, v in node.items() if not k.startswith('_')}
461
+
462
+ if not uid:
463
+ uid = str(uuid.uuid4())
464
+
465
+ name = props.get('name', str(uid))
466
+
467
+ if uid not in seen_nodes:
468
+ seen_nodes.add(uid)
469
+ color = "#97c2fc" # Default blue
470
+ if "Repository" == lbl: color = "#ffb3ba"
471
+ elif "File" == lbl: color = "#baffc9"
472
+ elif "Class" == lbl: color = "#bae1ff"
473
+ elif "Function" == lbl: color = "#ffffba"
474
+ elif "Module" == lbl: color = "#ffdfba"
475
+
476
+ data_nodes.append({
477
+ "id": uid,
478
+ "label": name,
479
+ "group": lbl,
480
+ "title": str(props),
481
+ "color": color
482
+ })
483
+ return uid
484
+
485
+ # Iterate results
486
+ for record in result:
487
+ # record is dict-like access to row items
488
+ n = record['n']
489
+ r = record['r']
490
+ m = record['m']
491
+
492
+ nid = process_node(n)
493
+ mid = process_node(m)
494
+
495
+ # Process Edge
496
+ e_type = 'REL'
497
+ if hasattr(r, 'type'):
498
+ e_type = r.type
499
+ elif isinstance(r, dict):
500
+ e_type = r.get('_label', 'REL')
501
+ elif hasattr(r, 'label'): # Some versions
502
+ e_type = r.label
503
+
504
+ data_edges.append({
505
+ "from": nid,
506
+ "to": mid,
507
+ "label": e_type,
508
+ "arrows": "to"
509
+ })
510
+
511
+ filename = "codegraph_viz.html"
512
+ html_content = f"""
513
+ <!DOCTYPE html>
514
+ <html>
515
+ <head>
516
+ <title>CodeGraphContext KùzuDB Visualization</title>
517
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
518
+ <style type="text/css">
519
+ #mynetwork {{
520
+ width: 100%;
521
+ height: 100vh;
522
+ border: 1px solid lightgray;
523
+ }}
524
+ </style>
525
+ </head>
526
+ <body>
527
+ <div id="mynetwork"></div>
528
+ <script type="text/javascript">
529
+ var nodes = new vis.DataSet({json.dumps(data_nodes)});
530
+ var edges = new vis.DataSet({json.dumps(data_edges)});
531
+ var container = document.getElementById('mynetwork');
532
+ var data = {{ nodes: nodes, edges: edges }};
533
+ var options = {{
534
+ nodes: {{ shape: 'dot', size: 16 }},
535
+ physics: {{ stabilization: false }},
536
+ layout: {{ improvedLayout: false }}
537
+ }};
538
+ var network = new vis.Network(container, data, options);
539
+ </script>
540
+ </body>
541
+ </html>
542
+ """
543
+
544
+ out_path = Path(filename).resolve()
545
+ with open(out_path, "w") as f:
546
+ f.write(html_content)
547
+
548
+ console.print(f"[green]Visualization generated at:[/green] {out_path}")
549
+ console.print("Opening in default browser...")
550
+ webbrowser.open(f"file://{out_path}")
551
+
552
+ except Exception as e:
553
+ console.print(f"[bold red]Visualization failed:[/bold red] {e}")
554
+ import traceback
555
+ traceback.print_exc()
556
+ finally:
557
+ db_manager.close_driver()
558
+
559
+
394
560
  def reindex_helper(path: str):
395
561
  """Force re-index by deleting and rebuilding the repository."""
396
562
  time_start = time.time()
@@ -48,7 +48,7 @@ DEFAULT_CONFIG = {
48
48
 
49
49
  # Configuration key descriptions
50
50
  CONFIG_DESCRIPTIONS = {
51
- "DEFAULT_DATABASE": "Default database backend (neo4j|falkordb)",
51
+ "DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb)",
52
52
  "FALKORDB_PATH": "Path to FalkorDB database file",
53
53
  "FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
54
54
  "INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
@@ -75,7 +75,7 @@ CONFIG_DESCRIPTIONS = {
75
75
 
76
76
  # Valid values for each config key
77
77
  CONFIG_VALIDATORS = {
78
- "DEFAULT_DATABASE": ["neo4j", "falkordb"],
78
+ "DEFAULT_DATABASE": ["neo4j", "falkordb", "kuzudb"],
79
79
  "INDEX_VARIABLES": ["true", "false"],
80
80
  "ALLOW_DB_DELETION": ["true", "false"],
81
81
  "DEBUG_LOGS": ["true", "false"],
@@ -229,6 +229,13 @@ def _load_credentials():
229
229
  from dotenv import dotenv_values
230
230
  from codegraphcontext.cli.config_manager import ensure_config_dir
231
231
 
232
+ # Capture DATABASE_TYPE from actual shell env BEFORE we load .env files.
233
+ # If the user ran `DATABASE_TYPE=falkordb cgc …` we must not let
234
+ # DEFAULT_DATABASE=neo4j in .env steal priority later.
235
+ shell_db_type = os.environ.get('DATABASE_TYPE')
236
+ if shell_db_type and not os.environ.get('CGC_RUNTIME_DB_TYPE'):
237
+ os.environ['CGC_RUNTIME_DB_TYPE'] = shell_db_type
238
+
232
239
  # Ensure config directory exists (lazy initialization)
233
240
  ensure_config_dir()
234
241
 
@@ -272,9 +279,16 @@ def _load_credentials():
272
279
  for config in config_sources:
273
280
  merged_config.update(config)
274
281
 
275
- # Apply merged config to environment
282
+ # Apply merged config to environment.
283
+ # IMPORTANT: DB-selection keys set in the shell must win over .env defaults.
284
+ # E.g. `DATABASE_TYPE=falkordb cgc index …` must not be overridden by
285
+ # DEFAULT_DATABASE=neo4j sitting in ~/.codegraphcontext/.env
286
+ DB_OVERRIDE_KEYS = {"DATABASE_TYPE", "CGC_RUNTIME_DB_TYPE", "DEFAULT_DATABASE"}
276
287
  for key, value in merged_config.items():
277
288
  if value is not None: # Only set non-None values
289
+ # Never let .env clobber a DB-type key that the user already set in the shell
290
+ if key in DB_OVERRIDE_KEYS and key in os.environ:
291
+ continue
278
292
  os.environ[key] = str(value)
279
293
 
280
294
  # Report what was loaded
@@ -287,14 +301,31 @@ def _load_credentials():
287
301
  console.print("[yellow]No configuration file found. Using defaults.[/yellow]")
288
302
 
289
303
 
290
- # Show which database is actually being used
291
- # Check for runtime override first (from -db/--database flag)
304
+ # Show which database is actually being used.
305
+ # When DATABASE_TYPE is explicitly set, trust it. When it's left to auto-
306
+ # detect, call get_database_manager() so the banner can never lie: e.g. if
307
+ # falkordblite is installed but its native .so is missing (frozen bundle),
308
+ # the factory falls back to KùzuDB and we display that correctly.
292
309
  runtime_db = os.environ.get("CGC_RUNTIME_DB_TYPE")
293
- if runtime_db:
294
- default_db = runtime_db.lower()
310
+ explicit_db = (
311
+ runtime_db
312
+ or os.environ.get("DEFAULT_DATABASE")
313
+ or os.environ.get("DATABASE_TYPE")
314
+ )
315
+
316
+ if explicit_db:
317
+ default_db = explicit_db.lower()
295
318
  else:
296
- default_db = os.environ.get("DEFAULT_DATABASE", "falkordb").lower()
297
-
319
+ # No explicit choice — ask the factory which backend it will use
320
+ try:
321
+ from codegraphcontext.core import get_database_manager
322
+ _mgr = get_database_manager()
323
+ default_db = _mgr.get_backend_type() # e.g. 'falkordb' / 'kuzudb'
324
+ except Exception:
325
+ # Factory failed entirely — still show a best-guess
326
+ from codegraphcontext.core import _is_falkordb_available
327
+ default_db = "falkordb" if _is_falkordb_available() else "kuzudb"
328
+
298
329
  if default_db == "neo4j":
299
330
  has_neo4j_creds = all([
300
331
  os.environ.get("NEO4J_URI"),
@@ -308,18 +339,26 @@ def _load_credentials():
308
339
  else:
309
340
  console.print("[cyan]Using database: Neo4j[/cyan]")
310
341
  else:
311
- console.print("[yellow]⚠ DEFAULT_DATABASE=neo4j but credentials not found. Falling back to FalkorDB.[/yellow]")
342
+ console.print("[yellow]⚠ DEFAULT_DATABASE=neo4j but credentials not found. Falling back to default.[/yellow]")
343
+ elif default_db == "falkordb":
344
+ console.print("[cyan]Using database: FalkorDB Lite[/cyan]")
345
+ elif default_db == "kuzudb":
346
+ console.print("[cyan]Using database: KùzuDB[/cyan]")
312
347
  elif default_db == "falkordb-remote":
313
348
  host = os.environ.get("FALKORDB_HOST")
314
349
  if host:
315
350
  console.print(f"[cyan]Using database: FalkorDB Remote ({host})[/cyan]")
316
351
  else:
317
352
  console.print("[yellow]⚠ DATABASE_TYPE=falkordb-remote but FALKORDB_HOST not set.[/yellow]")
318
- else:
353
+ elif default_db == "falkordb":
319
354
  if os.environ.get("FALKORDB_HOST"):
320
355
  console.print(f"[cyan]Using database: FalkorDB Remote ({os.environ.get('FALKORDB_HOST')})[/cyan]")
321
356
  else:
322
357
  console.print("[cyan]Using database: FalkorDB[/cyan]")
358
+ else:
359
+ console.print(f"[cyan]Using database: {default_db}[/cyan]")
360
+
361
+
323
362
 
324
363
  # ============================================================================
325
364
  # CONFIG COMMAND GROUP
@@ -2,26 +2,44 @@
2
2
  """
3
3
  Core database management module.
4
4
 
5
- Supports Neo4j, FalkorDB Lite, and remote FalkorDB backends.
5
+ Supports Neo4j, FalkorDB Lite, remote FalkorDB, and KùzuDB backends.
6
6
  Use DATABASE_TYPE environment variable to switch:
7
- - DATABASE_TYPE=falkordb - Uses embedded FalkorDB Lite (recommended for lite-version)
7
+ - DATABASE_TYPE=kuzudb - Uses embedded KùzuDB (Recommended for cross-platform zero-config)
8
+ - DATABASE_TYPE=falkordb - Uses embedded FalkorDB Lite (Unix-only)
8
9
  - DATABASE_TYPE=falkordb-remote - Uses a remote/hosted FalkorDB server over TCP
9
10
  - DATABASE_TYPE=neo4j - Uses Neo4j server
10
11
  - If not set, auto-detects based on what's available
12
+
13
+ Priority (no DATABASE_TYPE set):
14
+ 1. FalkorDB Lite (Unix + Python 3.12+ + falkordblite installed)
15
+ 2. KùzuDB (cross-platform fallback)
16
+ 3. Remote FalkorDB (if FALKORDB_HOST is set)
17
+ 4. Neo4j (if credentials are configured)
11
18
  """
12
19
  import os
20
+ import platform
13
21
  from typing import Union
14
22
 
15
23
  import importlib.util
16
24
 
25
+ def _is_kuzudb_available() -> bool:
26
+ """Check if KùzuDB is installed."""
27
+ try:
28
+ return importlib.util.find_spec("kuzu") is not None
29
+ except ImportError:
30
+ return False
31
+
17
32
  def _is_falkordb_available() -> bool:
18
- """Check if FalkorDB Lite is installed (without importing native modules)."""
33
+ """Check if FalkorDB Lite is installed (Unix only)."""
34
+ if platform.system() == "Windows":
35
+ return False
36
+
19
37
  import sys
20
38
  if sys.version_info < (3, 12):
21
39
  return False
22
40
  try:
23
- # Check for redislite/falkordb-client spec without loading it
24
- return importlib.util.find_spec("redislite") is not None
41
+ import redislite
42
+ return hasattr(redislite, 'falkordb_client')
25
43
  except ImportError:
26
44
  return False
27
45
 
@@ -37,7 +55,7 @@ def _is_neo4j_configured() -> bool:
37
55
  os.getenv('NEO4J_PASSWORD')
38
56
  ])
39
57
 
40
- def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager']:
58
+ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager']:
41
59
  """
42
60
  Factory function to get the appropriate database manager based on configuration.
43
61
 
@@ -45,12 +63,13 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
45
63
  1. Runtime Override: 'CGC_RUNTIME_DB_TYPE' (set via --database flag)
46
64
  2. Configured Default: 'DEFAULT_DATABASE' (set via 'cgc default database')
47
65
  3. Legacy Env Var: 'DATABASE_TYPE'
48
- 4. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
49
- 5. Implicit Default: FalkorDB Lite (if available)
50
- 6. Fallback: Neo4j (if configured)
66
+ 4. Implicit Default: KùzuDB (Best cross-platform zero-config)
67
+ 5. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
68
+ 6. Fallback Default: FalkorDB Lite (if Unix and available)
69
+ 7. Fallback: Neo4j (if configured)
51
70
  """
52
71
  from codegraphcontext.utils.debug_log import info_logger
53
-
72
+
54
73
  # 1. Runtime Override (CLI flag) or Config/Env
55
74
  db_type = os.getenv('CGC_RUNTIME_DB_TYPE')
56
75
  if not db_type:
@@ -60,13 +79,33 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
60
79
 
61
80
  if db_type:
62
81
  db_type = db_type.lower()
63
- if db_type == 'falkordb':
82
+ if db_type == 'kuzudb':
83
+ if not _is_kuzudb_available():
84
+ raise ValueError("Database set to 'kuzudb' but Kùzu is not installed.\nRun 'pip install kuzu'")
85
+ from .database_kuzu import KuzuDBManager
86
+ info_logger("Using KùzuDB (explicit)")
87
+ return KuzuDBManager()
88
+
89
+ elif db_type == 'falkordb':
64
90
  if not _is_falkordb_available():
65
- raise ValueError("Database set to 'falkordb' but FalkorDB Lite is not installed.\nRun 'pip install falkordblite'")
66
- from .database_falkordb import FalkorDBManager
67
- info_logger("Using FalkorDB Lite (explicit)")
68
- return FalkorDBManager()
91
+ info_logger("FalkorDB Lite is not supported or not installed. Falling back to KùzuDB.")
92
+ if _is_kuzudb_available():
93
+ from .database_kuzu import KuzuDBManager
94
+ return KuzuDBManager()
95
+ raise ValueError("Database set to 'falkordb' but FalkorDB Lite is not installed or not supported on this OS.\nRun 'pip install falkordblite'")
69
96
 
97
+ from .database_falkordb import FalkorDBManager, FalkorDBUnavailableError
98
+ try:
99
+ mgr = FalkorDBManager()
100
+ info_logger("Using FalkorDB Lite (explicit)")
101
+ return mgr
102
+ except FalkorDBUnavailableError as falkor_err:
103
+ info_logger(f"FalkorDB Lite not functional ({falkor_err}). Falling back to KùzuDB.")
104
+ if _is_kuzudb_available():
105
+ from .database_kuzu import KuzuDBManager
106
+ return KuzuDBManager()
107
+ raise
108
+
70
109
  elif db_type == 'falkordb-remote':
71
110
  if not _is_falkordb_remote_configured():
72
111
  raise ValueError(
@@ -79,52 +118,59 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
79
118
 
80
119
  elif db_type == 'neo4j':
81
120
  if not _is_neo4j_configured():
82
- raise ValueError("Database set to 'neo4j' but it is not configured.\nRun 'cgc neo4j setup' to configure Neo4j.")
121
+ raise ValueError("Database set to 'neo4j' but it is not configured.\nRun 'cgc neo4j setup' to configure Neo4j.")
83
122
  from .database import DatabaseManager
84
123
  info_logger("Using Neo4j Server (explicit)")
85
124
  return DatabaseManager()
86
125
  else:
87
- raise ValueError(f"Unknown database type: '{db_type}'. Use 'falkordb', 'falkordb-remote', or 'neo4j'.")
126
+ raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', or 'neo4j'.")
88
127
 
89
- # 4. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
128
+ # 4. Implicit Default -> FalkorDB Lite (Unix Zero Config)
129
+ if _is_falkordb_available():
130
+ from .database_falkordb import FalkorDBManager, FalkorDBUnavailableError
131
+ try:
132
+ mgr = FalkorDBManager()
133
+ info_logger("Using FalkorDB Lite (default)")
134
+ return mgr
135
+ except FalkorDBUnavailableError as falkor_err:
136
+ info_logger(
137
+ f"FalkorDB Lite not functional in this environment ({falkor_err}). "
138
+ "Falling back to KùzuDB."
139
+ )
140
+ # fall through to KùzuDB below
141
+
142
+ # 5. Implicit Default -> KùzuDB (Best Zero Config)
143
+ if _is_kuzudb_available():
144
+ from .database_kuzu import KuzuDBManager
145
+ info_logger("Using KùzuDB (default)")
146
+ return KuzuDBManager()
147
+
148
+ # 6. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
90
149
  if _is_falkordb_remote_configured():
91
150
  from .database_falkordb_remote import FalkorDBRemoteManager
92
151
  info_logger("Using remote FalkorDB (auto-detected via FALKORDB_HOST)")
93
152
  return FalkorDBRemoteManager()
94
153
 
95
- # 5. Implicit Default -> FalkorDB Lite (Zero Config)
96
- if _is_falkordb_available():
97
- from .database_falkordb import FalkorDBManager
98
- info_logger("Using FalkorDB Lite (default)")
99
- return FalkorDBManager()
100
-
101
- # 6. Fallback if FalkorDB missing but Neo4j is ready
154
+ # 7. Fallback if configured
102
155
  if _is_neo4j_configured():
103
156
  from .database import DatabaseManager
104
157
  info_logger("Using Neo4j Server (auto-detected)")
105
158
  return DatabaseManager()
106
159
 
107
- import sys
108
160
  error_msg = "No database backend available.\n"
109
-
110
- if sys.version_info < (3, 12):
111
- error_msg += (
112
- "FalkorDB Lite is not supported on Python < 3.12.\n"
113
- "You are running Python " + str(sys.version_info.major) + "." + str(sys.version_info.minor) + ".\n"
114
- "Please upgrade to Python 3.12+ to use the embedded database,\n"
115
- "OR run 'cgc neo4j setup' to configure an external Neo4j database."
116
- )
117
- else:
118
- error_msg += (
119
- "Recommended: Install FalkorDB Lite ('pip install falkordblite')\n"
120
- "Alternative: Run 'cgc neo4j setup' to configure Neo4j."
121
- )
122
-
161
+ error_msg += "Recommended: Install KùzuDB for zero-config ('pip install kuzu')\n"
162
+
163
+ if platform.system() != "Windows":
164
+ error_msg += "Alternative: Install FalkorDB Lite ('pip install falkordblite')\n"
165
+
166
+ error_msg += "Alternative: Run 'cgc neo4j setup' to configure Neo4j."
167
+
123
168
  raise ValueError(error_msg)
124
169
 
125
- # For backward compatibility, export DatabaseManager
170
+ # For backward compatibility, export managers
126
171
  from .database import DatabaseManager
127
172
  from .database_falkordb import FalkorDBManager
128
173
  from .database_falkordb_remote import FalkorDBRemoteManager
174
+ from .database_kuzu import KuzuDBManager
129
175
 
130
- __all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'get_database_manager']
176
+ __all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'get_database_manager']
@@ -3,6 +3,13 @@
3
3
  This module provides a thread-safe singleton manager for the FalkorDB Lite database connection.
4
4
  FalkorDB Lite is an embedded graph database that requires no external server setup.
5
5
  """
6
+
7
+ class FalkorDBUnavailableError(RuntimeError):
8
+ """
9
+ Raised when FalkorDB Lite is installed but cannot actually run in this
10
+ environment (e.g. falkordb.so not found in a PyInstaller bundle,
11
+ or GRAPH.QUERY not available). Callers should fall back to KùzuDB.
12
+ """
6
13
  import os
7
14
  import sys
8
15
  import subprocess
@@ -167,15 +174,16 @@ class FalkorDBManager:
167
174
  try:
168
175
  from falkordb import FalkorDB
169
176
  d = FalkorDB(unix_socket_path=self.socket_path)
170
- try:
171
- d.execute_command("PING")
172
- except AttributeError:
173
- pass
174
- info_logger("Connected to existing FalkorDB Lite process.")
177
+ # Test not just connectivity (PING), but functionality (GRAPH.QUERY)
178
+ # This ensures we don't connect to a "stale" process that doesn't have the module loaded
179
+ test_graph = d.select_graph('__cgc_health_check')
180
+ test_graph.query("RETURN 1")
181
+ info_logger("Connected to existing (functional) FalkorDB Lite process.")
175
182
  return
176
- except Exception:
177
- # Stale socket or unresponsive
178
- info_logger("Found stale socket, cleaning up...")
183
+ except Exception as e:
184
+ # Stale socket, unresponsive, or "brainless" (unknown command GRAPH.QUERY)
185
+ info_logger(f"Existing FalkorDB process at {self.socket_path} is stale or non-functional: {e}")
186
+ info_logger("Cleaning up and attempting fresh start...")
179
187
  try:
180
188
  os.remove(self.socket_path)
181
189
  except OSError:
@@ -190,7 +198,21 @@ class FalkorDBManager:
190
198
  python_exe = sys.executable
191
199
 
192
200
  # We assume codegraphcontext is installed or in python path
193
- cmd = [python_exe, '-m', 'codegraphcontext.core.falkor_worker']
201
+ if getattr(sys, 'frozen', False):
202
+ # In frozen mode, the executable is the bundle itself.
203
+ # We tell the bundle to run the worker instead of the app via environment variable.
204
+ env['CGC_RUN_FALKOR_WORKER'] = 'true'
205
+ cmd = [python_exe]
206
+ else:
207
+ # If not frozen, sys.executable should be python.
208
+ # But on some platforms (like PIP installs), it might be the 'cgc' entry point script.
209
+ # We check if it looks like python, otherwise search the PATH.
210
+ import shutil
211
+ exe_name = os.path.basename(python_exe).lower()
212
+ if not any(x in exe_name for x in ['python', 'py.exe', 'pypy']):
213
+ python_exe = shutil.which('python3') or shutil.which('python') or sys.executable
214
+
215
+ cmd = [python_exe, '-m', 'codegraphcontext.core.falkor_worker']
194
216
 
195
217
  info_logger("Starting FalkorDB Lite worker subprocess...")
196
218
  self._process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
@@ -209,7 +231,15 @@ class FalkorDBManager:
209
231
  # Check if process died
210
232
  if self._process.poll() is not None:
211
233
  out, err = self._process.communicate()
212
- raise RuntimeError(f"FalkorDB worker failed to start (Exit Code {self._process.returncode}):\nSTDOUT: {out.decode()}\nSTDERR: {err.decode()}")
234
+ returncode = self._process.returncode
235
+
236
+ # Any non-zero exit code during startup means this backend is toast
237
+ # Raise FalkorDBUnavailableError to trigger the automatic KùzuDB fallback
238
+ raise FalkorDBUnavailableError(
239
+ f"FalkorDB Lite worker failed to start (Exit Code {returncode}).\n"
240
+ f"STDOUT: {out.decode().strip()}\n"
241
+ f"STDERR: {err.decode().strip()}"
242
+ )
213
243
 
214
244
  time.sleep(0.5)
215
245
 
@@ -217,8 +247,10 @@ class FalkorDBManager:
217
247
 
218
248
  def close_driver(self):
219
249
  """Closes the connection."""
220
- self._driver = None
221
- self._graph = None
250
+ if self._driver is not None:
251
+ info_logger("Closing FalkorDB Lite connection")
252
+ self._driver = None
253
+ self._graph = None
222
254
 
223
255
  def shutdown(self):
224
256
  """Kills the subprocess on exit."""
@@ -114,8 +114,10 @@ class FalkorDBRemoteManager:
114
114
 
115
115
  def close_driver(self):
116
116
  """Closes the connection."""
117
- self._driver = None
118
- self._graph = None
117
+ if self._driver is not None:
118
+ info_logger("Closing FalkorDB Remote connection")
119
+ self._driver = None
120
+ self._graph = None
119
121
 
120
122
  def shutdown(self):
121
123
  """Clean up on exit. No subprocess to kill for remote connections."""