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.
- codegraphcontext/cli/cli_helpers.py +174 -8
- codegraphcontext/cli/config_manager.py +2 -2
- codegraphcontext/cli/main.py +48 -9
- codegraphcontext/core/__init__.py +88 -42
- codegraphcontext/core/database_falkordb.py +44 -12
- codegraphcontext/core/database_falkordb_remote.py +4 -2
- codegraphcontext/core/database_kuzu.py +456 -0
- codegraphcontext/core/falkor_worker.py +61 -5
- codegraphcontext/tools/code_finder.py +60 -45
- codegraphcontext/tools/graph_builder.py +140 -51
- codegraphcontext/tools/handlers/query_handlers.py +3 -2
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/METADATA +16 -13
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/RECORD +17 -16
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/WHEEL +1 -1
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/entry_points.txt +0 -0
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/licenses/LICENSE +0 -0
- {codegraphcontext-0.2.8.dist-info → codegraphcontext-0.2.9.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
274
|
-
|
|
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"],
|
codegraphcontext/cli/main.py
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
5
|
+
Supports Neo4j, FalkorDB Lite, remote FalkorDB, and KùzuDB backends.
|
|
6
6
|
Use DATABASE_TYPE environment variable to switch:
|
|
7
|
-
- DATABASE_TYPE=
|
|
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 (
|
|
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
|
-
|
|
24
|
-
return
|
|
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.
|
|
49
|
-
5.
|
|
50
|
-
6. Fallback:
|
|
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 == '
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
178
|
-
info_logger("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
221
|
-
|
|
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
|
|
118
|
-
|
|
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."""
|