codegraphcontext-rust 0.5.5__cp311-cp311-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraphcontext/__init__.py +2 -0
- codegraphcontext/__main__.py +5 -0
- codegraphcontext/_cgc_rust.cp311-win_amd64.pyd +0 -0
- codegraphcontext/cli/__init__.py +1 -0
- codegraphcontext/cli/cli_helpers.py +774 -0
- codegraphcontext/cli/config_manager.py +1051 -0
- codegraphcontext/cli/main.py +2386 -0
- codegraphcontext/cli/registry_commands.py +404 -0
- codegraphcontext/cli/setup_macos.py +93 -0
- codegraphcontext/cli/setup_wizard.py +991 -0
- codegraphcontext/cli/visualizer.py +50 -0
- codegraphcontext/core/__init__.py +165 -0
- codegraphcontext/core/bundle_registry.py +181 -0
- codegraphcontext/core/cgc_bundle.py +858 -0
- codegraphcontext/core/cgcignore.py +118 -0
- codegraphcontext/core/database.py +273 -0
- codegraphcontext/core/database_falkordb.py +480 -0
- codegraphcontext/core/database_falkordb_remote.py +199 -0
- codegraphcontext/core/database_kuzu.py +636 -0
- codegraphcontext/core/falkor_worker.py +132 -0
- codegraphcontext/core/jobs.py +132 -0
- codegraphcontext/core/watcher.py +260 -0
- codegraphcontext/prompts.py +125 -0
- codegraphcontext/server.py +471 -0
- codegraphcontext/tool_definitions.py +220 -0
- codegraphcontext/tools/__init__.py +0 -0
- codegraphcontext/tools/advanced_language_query_tool.py +103 -0
- codegraphcontext/tools/code_finder.py +1160 -0
- codegraphcontext/tools/graph_builder.py +337 -0
- codegraphcontext/tools/handlers/analysis_handlers.py +114 -0
- codegraphcontext/tools/handlers/indexing_handlers.py +116 -0
- codegraphcontext/tools/handlers/management_handlers.py +339 -0
- codegraphcontext/tools/handlers/query_handlers.py +83 -0
- codegraphcontext/tools/handlers/watcher_handlers.py +83 -0
- codegraphcontext/tools/indexing/__init__.py +1 -0
- codegraphcontext/tools/indexing/constants.py +25 -0
- codegraphcontext/tools/indexing/discovery.py +64 -0
- codegraphcontext/tools/indexing/engine.py +109 -0
- codegraphcontext/tools/indexing/persistence/__init__.py +3 -0
- codegraphcontext/tools/indexing/persistence/parquet_loader.py +643 -0
- codegraphcontext/tools/indexing/persistence/writer.py +1114 -0
- codegraphcontext/tools/indexing/pipeline.py +171 -0
- codegraphcontext/tools/indexing/pre_scan.py +105 -0
- codegraphcontext/tools/indexing/resolution/__init__.py +8 -0
- codegraphcontext/tools/indexing/resolution/calls.py +253 -0
- codegraphcontext/tools/indexing/resolution/inheritance.py +91 -0
- codegraphcontext/tools/indexing/sanitize.py +41 -0
- codegraphcontext/tools/indexing/schema.py +79 -0
- codegraphcontext/tools/indexing/schema_contract.py +44 -0
- codegraphcontext/tools/indexing/scip_pipeline.py +140 -0
- codegraphcontext/tools/languages/c.py +563 -0
- codegraphcontext/tools/languages/cpp.py +616 -0
- codegraphcontext/tools/languages/csharp.py +551 -0
- codegraphcontext/tools/languages/dart.py +378 -0
- codegraphcontext/tools/languages/elixir.py +460 -0
- codegraphcontext/tools/languages/go.py +499 -0
- codegraphcontext/tools/languages/haskell.py +426 -0
- codegraphcontext/tools/languages/java.py +500 -0
- codegraphcontext/tools/languages/javascript.py +589 -0
- codegraphcontext/tools/languages/kotlin.py +640 -0
- codegraphcontext/tools/languages/perl.py +260 -0
- codegraphcontext/tools/languages/php.py +519 -0
- codegraphcontext/tools/languages/python.py +576 -0
- codegraphcontext/tools/languages/ruby.py +537 -0
- codegraphcontext/tools/languages/rust.py +295 -0
- codegraphcontext/tools/languages/scala.py +519 -0
- codegraphcontext/tools/languages/swift.py +491 -0
- codegraphcontext/tools/languages/typescript.py +575 -0
- codegraphcontext/tools/languages/typescriptjsx.py +151 -0
- codegraphcontext/tools/package_resolver.py +472 -0
- codegraphcontext/tools/query_tool_languages/c_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/cpp_toolkit.py +87 -0
- codegraphcontext/tools/query_tool_languages/csharp_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/dart_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/go_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/haskell_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/java_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/javascript_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/perl_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/python_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/ruby_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/rust_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/scala_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/swift_toolkit.py +5 -0
- codegraphcontext/tools/query_tool_languages/typescript_toolkit.py +5 -0
- codegraphcontext/tools/scip_indexer.py +467 -0
- codegraphcontext/tools/scip_pb2.py +2455 -0
- codegraphcontext/tools/system.py +133 -0
- codegraphcontext/tools/tree_sitter_parser.py +103 -0
- codegraphcontext/utils/debug_log.py +90 -0
- codegraphcontext/utils/path_ignore.py +55 -0
- codegraphcontext/utils/repo_path.py +27 -0
- codegraphcontext/utils/tree_sitter_manager.py +285 -0
- codegraphcontext/utils/visualize_graph.py +0 -0
- codegraphcontext/viz/server.py +282 -0
- codegraphcontext_rust-0.5.5.dist-info/METADATA +537 -0
- codegraphcontext_rust-0.5.5.dist-info/RECORD +101 -0
- codegraphcontext_rust-0.5.5.dist-info/WHEEL +4 -0
- codegraphcontext_rust-0.5.5.dist-info/entry_points.txt +3 -0
- codegraphcontext_rust-0.5.5.dist-info/licenses/LICENSE +21 -0
- codegraphcontext_rust-0.5.5.dist-info/sboms/cgc-python.cyclonedx.json +2333 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# src/codegraphcontext/core/database_falkordb.py
|
|
2
|
+
"""
|
|
3
|
+
This module provides a thread-safe singleton manager for the FalkorDB Lite database connection.
|
|
4
|
+
FalkorDB Lite is an embedded graph database that requires no external server setup.
|
|
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
|
+
"""
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import subprocess
|
|
16
|
+
import time
|
|
17
|
+
import atexit
|
|
18
|
+
import threading
|
|
19
|
+
import re
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Optional, Tuple
|
|
22
|
+
|
|
23
|
+
from codegraphcontext.utils.debug_log import debug_log, info_logger, error_logger, warning_logger
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Compatibility patch: redis-py >= 5.x added OpenTelemetry error telemetry that
|
|
27
|
+
# accesses conn.port inside its error handler. UnixDomainSocketConnection never
|
|
28
|
+
# had a 'port' attribute, so any exception raised during a Unix-socket connection
|
|
29
|
+
# (e.g. the sentinel-detection INFO call inside FalkorDB.__init__) would produce
|
|
30
|
+
# a secondary AttributeError masking the real problem.
|
|
31
|
+
# Patching the class at import time costs nothing and fixes all call-sites.
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
try:
|
|
34
|
+
from redis.connection import UnixDomainSocketConnection as _UDSC
|
|
35
|
+
if not hasattr(_UDSC, 'port'):
|
|
36
|
+
_UDSC.port = 0 # type: ignore[attr-defined]
|
|
37
|
+
except Exception:
|
|
38
|
+
pass # redis not installed or class structure changed — safe to ignore
|
|
39
|
+
|
|
40
|
+
class FalkorDBManager:
|
|
41
|
+
"""
|
|
42
|
+
Manages the FalkorDB Lite database connection as a singleton.
|
|
43
|
+
Uses a subprocess to isolate the embedded database from the main process environment.
|
|
44
|
+
"""
|
|
45
|
+
_instance = None
|
|
46
|
+
_process = None
|
|
47
|
+
_driver = None
|
|
48
|
+
_graph = None
|
|
49
|
+
_lock = threading.Lock()
|
|
50
|
+
|
|
51
|
+
def __new__(cls, *args, **kwargs):
|
|
52
|
+
"""Standard singleton pattern implementation."""
|
|
53
|
+
if cls._instance is None:
|
|
54
|
+
with cls._lock:
|
|
55
|
+
if cls._instance is None:
|
|
56
|
+
cls._instance = super(FalkorDBManager, cls).__new__(cls)
|
|
57
|
+
return cls._instance
|
|
58
|
+
|
|
59
|
+
def __init__(self, db_path: Optional[str] = None, socket_path: Optional[str] = None):
|
|
60
|
+
"""
|
|
61
|
+
Initializes the manager with default database path or explicit overrides.
|
|
62
|
+
The `_initialized` flag prevents re-initialization on subsequent calls.
|
|
63
|
+
"""
|
|
64
|
+
# If we have an existing instance but are being asked to connect to a different path
|
|
65
|
+
# we need to be careful — it's a singleton. For the new context system, we ensure
|
|
66
|
+
# the singleton instance gets re-initialized if the path changes.
|
|
67
|
+
if hasattr(self, '_initialized') and self.db_path == db_path:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
self._initialized = False
|
|
71
|
+
|
|
72
|
+
# Configuration priority:
|
|
73
|
+
# 1. Environment variable (highest priority)
|
|
74
|
+
# 2. Config manager (supports project-local .env)
|
|
75
|
+
# 3. Default path (lowest priority)
|
|
76
|
+
|
|
77
|
+
# Try to load from config manager
|
|
78
|
+
try:
|
|
79
|
+
from codegraphcontext.cli.config_manager import get_config_value
|
|
80
|
+
config_db_path = get_config_value('FALKORDB_PATH')
|
|
81
|
+
config_socket_path = get_config_value('FALKORDB_SOCKET_PATH')
|
|
82
|
+
except Exception:
|
|
83
|
+
# Config manager not available or error loading
|
|
84
|
+
config_db_path = None
|
|
85
|
+
config_socket_path = None
|
|
86
|
+
|
|
87
|
+
# Database path with fallback chain (Explicit > Env > Config/Default)
|
|
88
|
+
self.db_path = db_path or os.getenv(
|
|
89
|
+
'FALKORDB_PATH',
|
|
90
|
+
config_db_path or str(Path.home() / '.codegraphcontext' / 'global' / 'falkordb.db')
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Socket path with fallback chain
|
|
94
|
+
if socket_path:
|
|
95
|
+
self.socket_path = socket_path
|
|
96
|
+
elif db_path:
|
|
97
|
+
# If a custom DB path was given but no socket path, infer socket path automatically
|
|
98
|
+
# near the custom database rather than putting it in the global directory.
|
|
99
|
+
db_dir = Path(db_path).parent
|
|
100
|
+
self.socket_path = str(db_dir / 'falkordb.sock')
|
|
101
|
+
else:
|
|
102
|
+
self.socket_path = os.getenv(
|
|
103
|
+
'FALKORDB_SOCKET_PATH',
|
|
104
|
+
config_socket_path or str(Path.home() / '.codegraphcontext' / 'global' / 'falkordb.sock')
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.graph_name = os.getenv('FALKORDB_GRAPH_NAME', 'codegraph')
|
|
108
|
+
self._initialized = True
|
|
109
|
+
|
|
110
|
+
# Register cleanup on exit
|
|
111
|
+
atexit.register(self.shutdown)
|
|
112
|
+
|
|
113
|
+
def get_driver(self):
|
|
114
|
+
"""
|
|
115
|
+
Gets the FalkorDB connection, starting the subprocess if necessary.
|
|
116
|
+
This method is thread-safe.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A FalkorDB graph instance that mimics Neo4j driver interface.
|
|
120
|
+
"""
|
|
121
|
+
import platform
|
|
122
|
+
|
|
123
|
+
if platform.system() == "Windows":
|
|
124
|
+
raise RuntimeError(
|
|
125
|
+
"CodeGraphContext uses redislite/FalkorDB, which does not support Windows.\n"
|
|
126
|
+
"Please run the project using WSL or Docker."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if self._driver is None:
|
|
130
|
+
if sys.version_info < (3, 12):
|
|
131
|
+
raise ValueError("FalkorDB Lite is not supported on Python < 3.12.")
|
|
132
|
+
|
|
133
|
+
with self._lock:
|
|
134
|
+
if self._driver is None:
|
|
135
|
+
# CRITICAL FIX: Prevent ~/.local/bin/falkordb.so from shadowing falkordb package
|
|
136
|
+
# When running via 'cgc' script installed in ~/.local/bin, sys.path[0] is that dir.
|
|
137
|
+
if sys.path and sys.path[0]:
|
|
138
|
+
potential_shadow = os.path.join(sys.path[0], 'falkordb.so')
|
|
139
|
+
if os.path.exists(potential_shadow):
|
|
140
|
+
info_logger("Detected 'falkordb.so' in sys.path[0]. Removing path to prevent import shadowing.")
|
|
141
|
+
sys.path.pop(0)
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
self._ensure_server_running()
|
|
145
|
+
|
|
146
|
+
# Use Official FalkorDB Client to connect to the socket
|
|
147
|
+
from falkordb import FalkorDB
|
|
148
|
+
|
|
149
|
+
info_logger(f"Connecting to FalkorDB Lite at {self.socket_path}")
|
|
150
|
+
self._driver = FalkorDB(unix_socket_path=self.socket_path)
|
|
151
|
+
self._graph = self._driver.select_graph(self.graph_name)
|
|
152
|
+
|
|
153
|
+
# Test the connection
|
|
154
|
+
try:
|
|
155
|
+
# Graph creation is lazy in some clients, force a query
|
|
156
|
+
self._graph.query("RETURN 1")
|
|
157
|
+
info_logger(f"FalkorDB Lite connection established successfully")
|
|
158
|
+
info_logger(f"Graph name: {self.graph_name}")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
info_logger(f"Initial ping check: {e}")
|
|
161
|
+
|
|
162
|
+
except ImportError as e:
|
|
163
|
+
error_logger(
|
|
164
|
+
"FalkorDB client is not installed. Install it with:\n"
|
|
165
|
+
" pip install falkordblite"
|
|
166
|
+
)
|
|
167
|
+
raise ValueError("FalkorDB client missing.") from e
|
|
168
|
+
except Exception as e:
|
|
169
|
+
error_logger(f"Failed to initialize FalkorDB: {e}")
|
|
170
|
+
raise
|
|
171
|
+
|
|
172
|
+
# Return a wrapper that provides Neo4j-like session interface
|
|
173
|
+
return FalkorDBDriverWrapper(self._graph)
|
|
174
|
+
|
|
175
|
+
def _ensure_server_running(self):
|
|
176
|
+
"""Starts the FalkorDB worker subprocess if not reachable."""
|
|
177
|
+
import platform
|
|
178
|
+
|
|
179
|
+
if platform.system() == "Windows":
|
|
180
|
+
raise RuntimeError(
|
|
181
|
+
"CodeGraphContext uses redislite/FalkorDB, which does not support Windows.\n"
|
|
182
|
+
"Please run the project using WSL or Docker."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 1. Try to connect first (maybe running from previous session or other process)
|
|
186
|
+
if os.path.exists(self.socket_path):
|
|
187
|
+
try:
|
|
188
|
+
from falkordb import FalkorDB
|
|
189
|
+
d = FalkorDB(unix_socket_path=self.socket_path)
|
|
190
|
+
# Test not just connectivity (PING), but functionality (GRAPH.QUERY)
|
|
191
|
+
# This ensures we don't connect to a "stale" process that doesn't have the module loaded
|
|
192
|
+
test_graph = d.select_graph('__cgc_health_check')
|
|
193
|
+
test_graph.query("RETURN 1")
|
|
194
|
+
info_logger("Connected to existing (functional) FalkorDB Lite process.")
|
|
195
|
+
return
|
|
196
|
+
except Exception as e:
|
|
197
|
+
# Stale socket, unresponsive, or "brainless" (unknown command GRAPH.QUERY)
|
|
198
|
+
info_logger(f"Existing FalkorDB process at {self.socket_path} is stale or non-functional: {e}")
|
|
199
|
+
info_logger("Cleaning up and attempting fresh start...")
|
|
200
|
+
try:
|
|
201
|
+
os.remove(self.socket_path)
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
# 2. Start Subprocess
|
|
206
|
+
env = os.environ.copy()
|
|
207
|
+
env['FALKORDB_PATH'] = self.db_path
|
|
208
|
+
env['FALKORDB_SOCKET_PATH'] = self.socket_path
|
|
209
|
+
|
|
210
|
+
# Determine python executable
|
|
211
|
+
python_exe = sys.executable
|
|
212
|
+
|
|
213
|
+
# We assume codegraphcontext is installed or in python path
|
|
214
|
+
if getattr(sys, 'frozen', False):
|
|
215
|
+
# In frozen mode, the executable is the bundle itself.
|
|
216
|
+
# We tell the bundle to run the worker instead of the app via environment variable.
|
|
217
|
+
env['CGC_RUN_FALKOR_WORKER'] = 'true'
|
|
218
|
+
cmd = [python_exe]
|
|
219
|
+
else:
|
|
220
|
+
# If not frozen, sys.executable should be python.
|
|
221
|
+
# But on some platforms (like PIP installs), it might be the 'cgc' entry point script.
|
|
222
|
+
# We check if it looks like python, otherwise search the PATH.
|
|
223
|
+
import shutil
|
|
224
|
+
exe_name = os.path.basename(python_exe).lower()
|
|
225
|
+
if not any(x in exe_name for x in ['python', 'py.exe', 'pypy']):
|
|
226
|
+
python_exe = shutil.which('python3') or shutil.which('python') or sys.executable
|
|
227
|
+
|
|
228
|
+
cmd = [python_exe, '-m', 'codegraphcontext.core.falkor_worker']
|
|
229
|
+
|
|
230
|
+
info_logger("Starting FalkorDB Lite worker subprocess...")
|
|
231
|
+
self._process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
232
|
+
|
|
233
|
+
# 3. Wait for Readiness
|
|
234
|
+
start_time = time.time()
|
|
235
|
+
timeout = 20 # seconds
|
|
236
|
+
|
|
237
|
+
while time.time() - start_time < timeout:
|
|
238
|
+
if os.path.exists(self.socket_path):
|
|
239
|
+
# Socket created!
|
|
240
|
+
# Give it a tiny sleep to ensure listening
|
|
241
|
+
time.sleep(0.2)
|
|
242
|
+
return
|
|
243
|
+
|
|
244
|
+
# Check if process died
|
|
245
|
+
if self._process.poll() is not None:
|
|
246
|
+
out, err = self._process.communicate()
|
|
247
|
+
returncode = self._process.returncode
|
|
248
|
+
|
|
249
|
+
# Any non-zero exit code during startup means this backend is toast
|
|
250
|
+
# Raise FalkorDBUnavailableError to trigger the automatic KùzuDB fallback
|
|
251
|
+
raise FalkorDBUnavailableError(
|
|
252
|
+
f"FalkorDB Lite worker failed to start (Exit Code {returncode}).\n"
|
|
253
|
+
f"STDOUT: {out.decode().strip()}\n"
|
|
254
|
+
f"STDERR: {err.decode().strip()}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
time.sleep(0.5)
|
|
258
|
+
|
|
259
|
+
raise RuntimeError("Timed out waiting for FalkorDB Lite to start.")
|
|
260
|
+
|
|
261
|
+
def close_driver(self):
|
|
262
|
+
"""Closes the connection."""
|
|
263
|
+
if self._driver is not None:
|
|
264
|
+
info_logger("Closing FalkorDB Lite connection")
|
|
265
|
+
self._driver = None
|
|
266
|
+
self._graph = None
|
|
267
|
+
|
|
268
|
+
def shutdown(self):
|
|
269
|
+
"""Kills the subprocess on exit."""
|
|
270
|
+
if self._process:
|
|
271
|
+
if self._process.poll() is None:
|
|
272
|
+
info_logger("Stopping FalkorDB subprocess...")
|
|
273
|
+
self._process.terminate()
|
|
274
|
+
try:
|
|
275
|
+
self._process.wait(timeout=5)
|
|
276
|
+
except subprocess.TimeoutExpired:
|
|
277
|
+
self._process.kill()
|
|
278
|
+
|
|
279
|
+
def is_connected(self) -> bool:
|
|
280
|
+
"""Checks if the database connection is currently active."""
|
|
281
|
+
if self._graph is None:
|
|
282
|
+
return False
|
|
283
|
+
try:
|
|
284
|
+
self._graph.query("RETURN 1")
|
|
285
|
+
return True
|
|
286
|
+
except Exception:
|
|
287
|
+
return False
|
|
288
|
+
|
|
289
|
+
def get_backend_type(self) -> str:
|
|
290
|
+
"""Returns the database backend type."""
|
|
291
|
+
return 'falkordb'
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def validate_config(db_path: str = None) -> Tuple[bool, Optional[str]]:
|
|
296
|
+
"""
|
|
297
|
+
Validates FalkorDB configuration parameters.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
301
|
+
"""
|
|
302
|
+
if db_path:
|
|
303
|
+
db_dir = Path(db_path).parent
|
|
304
|
+
if not os.access(db_dir, os.W_OK) and db_dir.exists():
|
|
305
|
+
return False, (
|
|
306
|
+
f"Cannot write to directory: {db_dir}\n"
|
|
307
|
+
"Please ensure you have write permissions."
|
|
308
|
+
)
|
|
309
|
+
return True, None
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def test_connection(db_path: str = None) -> Tuple[bool, Optional[str]]:
|
|
313
|
+
"""
|
|
314
|
+
Tests the FalkorDB Lite connection availability.
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
if sys.version_info < (3, 12):
|
|
318
|
+
return False, "FalkorDB Lite is not supported on Python < 3.12. Please upgrade or use Neo4j."
|
|
319
|
+
|
|
320
|
+
import falkordb
|
|
321
|
+
return True, None
|
|
322
|
+
except ImportError:
|
|
323
|
+
return False, (
|
|
324
|
+
"FalkorDB client is not installed.\n"
|
|
325
|
+
"Install it with: pip install falkordblite"
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class FalkorDBDriverWrapper:
|
|
330
|
+
"""
|
|
331
|
+
Wrapper class to provide Neo4j driver-like interface for FalkorDB Lite.
|
|
332
|
+
This allows existing code to work with minimal changes.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(self, graph):
|
|
336
|
+
self.graph = graph
|
|
337
|
+
|
|
338
|
+
def session(self):
|
|
339
|
+
"""Returns a session-like object for FalkorDB."""
|
|
340
|
+
return FalkorDBSessionWrapper(self.graph)
|
|
341
|
+
|
|
342
|
+
def close(self):
|
|
343
|
+
"""FalkorDB Lite doesn't need explicit close for sessions."""
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class FalkorDBSessionWrapper:
|
|
348
|
+
"""
|
|
349
|
+
Wrapper class to provide Neo4j session-like interface for FalkorDB Lite.
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
def __init__(self, graph):
|
|
353
|
+
self.graph = graph
|
|
354
|
+
|
|
355
|
+
def run(self, query, **parameters):
|
|
356
|
+
"""
|
|
357
|
+
Execute a Cypher query on FalkorDB.
|
|
358
|
+
"""
|
|
359
|
+
# Translate Neo4j schema queries to FalkorDB syntax
|
|
360
|
+
query = self._translate_schema_query(query)
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
result = self.graph.query(query, parameters)
|
|
364
|
+
return FalkorDBResultWrapper(result)
|
|
365
|
+
except Exception as e:
|
|
366
|
+
# Ignore errors about existing constraints/indexes
|
|
367
|
+
error_msg = str(e).lower()
|
|
368
|
+
if "already exists" in error_msg or "already created" in error_msg:
|
|
369
|
+
return FalkorDBResultWrapper(None)
|
|
370
|
+
|
|
371
|
+
error_logger(f"FalkorDB query failed: {query[:100]}... Error: {e}")
|
|
372
|
+
raise
|
|
373
|
+
|
|
374
|
+
def _translate_schema_query(self, query: str) -> str:
|
|
375
|
+
"""Translate Neo4j schema queries to FalkorDB/RedisGraph syntax."""
|
|
376
|
+
q_upper = query.upper()
|
|
377
|
+
|
|
378
|
+
# Handle Fulltext Indexes (Not supported in same syntax, skip for now)
|
|
379
|
+
if "CREATE FULLTEXT INDEX" in q_upper:
|
|
380
|
+
return "RETURN 1"
|
|
381
|
+
|
|
382
|
+
# Handle Constraints
|
|
383
|
+
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)
|
|
402
|
+
|
|
403
|
+
# Handle Regular Indexes
|
|
404
|
+
elif "CREATE INDEX" in q_upper:
|
|
405
|
+
# Remove "IF NOT EXISTS"
|
|
406
|
+
query = re.sub(r'\s+IF NOT EXISTS', '', query, flags=re.IGNORECASE)
|
|
407
|
+
# Remove Index Name: CREATE INDEX name FOR -> CREATE INDEX FOR
|
|
408
|
+
query = re.sub(r'CREATE INDEX\s+\w+\s+FOR', 'CREATE INDEX FOR', query, flags=re.IGNORECASE)
|
|
409
|
+
|
|
410
|
+
return query
|
|
411
|
+
|
|
412
|
+
def __enter__(self):
|
|
413
|
+
return self
|
|
414
|
+
|
|
415
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class FalkorDBRecord(dict):
|
|
420
|
+
"""
|
|
421
|
+
Dict wrapper that provides a .data() method for compatibility with Neo4j records.
|
|
422
|
+
"""
|
|
423
|
+
def data(self):
|
|
424
|
+
return self
|
|
425
|
+
|
|
426
|
+
class FalkorDBResultWrapper:
|
|
427
|
+
"""
|
|
428
|
+
Wrapper class to provide Neo4j result-like interface for FalkorDB results.
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
def __init__(self, result):
|
|
432
|
+
self.result = result
|
|
433
|
+
self._consumed = False
|
|
434
|
+
|
|
435
|
+
def consume(self):
|
|
436
|
+
"""Mark result as consumed (for compatibility)."""
|
|
437
|
+
self._consumed = True
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
def single(self):
|
|
441
|
+
"""Return single result record as a FalkorDBRecord."""
|
|
442
|
+
data = self.data()
|
|
443
|
+
return data[0] if data else None
|
|
444
|
+
|
|
445
|
+
def data(self):
|
|
446
|
+
"""Return all results as list of FalkorDBRecord objects."""
|
|
447
|
+
if not hasattr(self.result, 'result_set'):
|
|
448
|
+
return []
|
|
449
|
+
|
|
450
|
+
# Convert result_set to list of dicts (wrapped in FalkorDBRecord)
|
|
451
|
+
results = []
|
|
452
|
+
if hasattr(self.result, 'header') and self.result.header:
|
|
453
|
+
headers = self.result.header
|
|
454
|
+
for row in self.result.result_set:
|
|
455
|
+
row_dict = FalkorDBRecord()
|
|
456
|
+
for i, header in enumerate(headers):
|
|
457
|
+
if i < len(row):
|
|
458
|
+
# FalkorDB headers are [column_type, column_name] pairs
|
|
459
|
+
# Extract the column name (index 1) and decode if bytes
|
|
460
|
+
if isinstance(header, (list, tuple)) and len(header) > 1:
|
|
461
|
+
header_name = header[1]
|
|
462
|
+
if isinstance(header_name, bytes):
|
|
463
|
+
header_name = header_name.decode('utf-8')
|
|
464
|
+
else:
|
|
465
|
+
header_name = str(header)
|
|
466
|
+
row_dict[header_name] = row[i]
|
|
467
|
+
results.append(row_dict)
|
|
468
|
+
elif hasattr(self.result, 'result_set'):
|
|
469
|
+
# Fallback if no header
|
|
470
|
+
for row in self.result.result_set:
|
|
471
|
+
if isinstance(row, (list, tuple)) and len(row) == 1:
|
|
472
|
+
results.append(FalkorDBRecord({'value': row[0]}))
|
|
473
|
+
else:
|
|
474
|
+
results.append(FalkorDBRecord({'value': row}))
|
|
475
|
+
|
|
476
|
+
return results
|
|
477
|
+
|
|
478
|
+
def __iter__(self):
|
|
479
|
+
"""Iterate over results as FalkorDBRecord objects."""
|
|
480
|
+
return iter(self.data())
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# src/codegraphcontext/core/database_falkordb_remote.py
|
|
2
|
+
"""
|
|
3
|
+
This module provides a thread-safe singleton manager for connecting to a
|
|
4
|
+
remote/hosted FalkorDB instance over TCP.
|
|
5
|
+
|
|
6
|
+
Unlike database_falkordb.py (which uses an embedded FalkorDB Lite via Unix sockets
|
|
7
|
+
and subprocesses), this module connects to an external FalkorDB server using
|
|
8
|
+
standard TCP connections. This is suitable for hosted FalkorDB instances
|
|
9
|
+
(e.g., FalkorDB Cloud) or self-hosted remote servers.
|
|
10
|
+
|
|
11
|
+
Configuration is read from environment variables:
|
|
12
|
+
- FALKORDB_HOST: Hostname of the FalkorDB server (required)
|
|
13
|
+
- FALKORDB_PORT: Port number (default: 6379)
|
|
14
|
+
- FALKORDB_PASSWORD: Authentication password (optional)
|
|
15
|
+
- FALKORDB_USERNAME: Authentication username (optional, default: None)
|
|
16
|
+
- FALKORDB_SSL: Enable SSL/TLS (default: false)
|
|
17
|
+
- FALKORDB_GRAPH_NAME: Graph name to use (default: codegraph)
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
import atexit
|
|
21
|
+
import threading
|
|
22
|
+
from typing import Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from codegraphcontext.utils.debug_log import info_logger, error_logger
|
|
25
|
+
|
|
26
|
+
# Reuse the Neo4j-compatible wrapper classes from the embedded FalkorDB module
|
|
27
|
+
from codegraphcontext.core.database_falkordb import (
|
|
28
|
+
FalkorDBDriverWrapper,
|
|
29
|
+
FalkorDBSessionWrapper,
|
|
30
|
+
FalkorDBResultWrapper,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FalkorDBRemoteManager:
|
|
35
|
+
"""
|
|
36
|
+
Manages a remote FalkorDB database connection as a singleton.
|
|
37
|
+
Connects via TCP — no subprocess, no Unix socket.
|
|
38
|
+
"""
|
|
39
|
+
_instance = None
|
|
40
|
+
_driver = None
|
|
41
|
+
_graph = None
|
|
42
|
+
_lock = threading.Lock()
|
|
43
|
+
|
|
44
|
+
def __new__(cls):
|
|
45
|
+
if cls._instance is None:
|
|
46
|
+
with cls._lock:
|
|
47
|
+
if cls._instance is None:
|
|
48
|
+
cls._instance = super(FalkorDBRemoteManager, cls).__new__(cls)
|
|
49
|
+
return cls._instance
|
|
50
|
+
|
|
51
|
+
def __init__(self):
|
|
52
|
+
if hasattr(self, '_initialized'):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
self.host = os.getenv('FALKORDB_HOST', 'localhost')
|
|
56
|
+
self.port = int(os.getenv('FALKORDB_PORT', '6379'))
|
|
57
|
+
self.password = os.getenv('FALKORDB_PASSWORD') or None
|
|
58
|
+
self.username = os.getenv('FALKORDB_USERNAME') or None
|
|
59
|
+
self.ssl = os.getenv('FALKORDB_SSL', 'false').lower() in ('true', '1', 'yes')
|
|
60
|
+
self.graph_name = os.getenv('FALKORDB_GRAPH_NAME', 'codegraph')
|
|
61
|
+
self._initialized = True
|
|
62
|
+
|
|
63
|
+
atexit.register(self.shutdown)
|
|
64
|
+
|
|
65
|
+
def get_driver(self):
|
|
66
|
+
"""
|
|
67
|
+
Gets the remote FalkorDB connection, creating it if necessary.
|
|
68
|
+
Thread-safe.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A FalkorDBDriverWrapper that provides a Neo4j-like session interface.
|
|
72
|
+
"""
|
|
73
|
+
if self._driver is None:
|
|
74
|
+
with self._lock:
|
|
75
|
+
if self._driver is None:
|
|
76
|
+
try:
|
|
77
|
+
from falkordb import FalkorDB
|
|
78
|
+
|
|
79
|
+
info_logger(
|
|
80
|
+
f"Connecting to remote FalkorDB at {self.host}:{self.port} "
|
|
81
|
+
f"(ssl={self.ssl})"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
kwargs = {
|
|
85
|
+
'host': self.host,
|
|
86
|
+
'port': self.port,
|
|
87
|
+
}
|
|
88
|
+
if self.password:
|
|
89
|
+
kwargs['password'] = self.password
|
|
90
|
+
if self.username:
|
|
91
|
+
kwargs['username'] = self.username
|
|
92
|
+
if self.ssl:
|
|
93
|
+
kwargs['ssl'] = True
|
|
94
|
+
|
|
95
|
+
self._driver = FalkorDB(**kwargs)
|
|
96
|
+
self._graph = self._driver.select_graph(self.graph_name)
|
|
97
|
+
|
|
98
|
+
# Verify connectivity
|
|
99
|
+
self._graph.query("RETURN 1")
|
|
100
|
+
info_logger("Remote FalkorDB connection established successfully")
|
|
101
|
+
info_logger(f"Graph name: {self.graph_name}")
|
|
102
|
+
|
|
103
|
+
except ImportError as e:
|
|
104
|
+
error_logger(
|
|
105
|
+
"FalkorDB client is not installed. Install it with:\n"
|
|
106
|
+
" pip install falkordb"
|
|
107
|
+
)
|
|
108
|
+
raise ValueError("FalkorDB client missing.") from e
|
|
109
|
+
except Exception as e:
|
|
110
|
+
error_logger(f"Failed to connect to remote FalkorDB: {e}")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
return FalkorDBDriverWrapper(self._graph)
|
|
114
|
+
|
|
115
|
+
def close_driver(self):
|
|
116
|
+
"""Closes the connection."""
|
|
117
|
+
if self._driver is not None:
|
|
118
|
+
info_logger("Closing FalkorDB Remote connection")
|
|
119
|
+
self._driver = None
|
|
120
|
+
self._graph = None
|
|
121
|
+
|
|
122
|
+
def shutdown(self):
|
|
123
|
+
"""Clean up on exit. No subprocess to kill for remote connections."""
|
|
124
|
+
self.close_driver()
|
|
125
|
+
|
|
126
|
+
def is_connected(self) -> bool:
|
|
127
|
+
"""Checks if the database connection is currently active."""
|
|
128
|
+
if self._graph is None:
|
|
129
|
+
return False
|
|
130
|
+
try:
|
|
131
|
+
self._graph.query("RETURN 1")
|
|
132
|
+
return True
|
|
133
|
+
except Exception:
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def get_backend_type(self) -> str:
|
|
137
|
+
"""Returns the database backend type."""
|
|
138
|
+
return 'falkordb-remote'
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def validate_config() -> Tuple[bool, Optional[str]]:
|
|
142
|
+
"""
|
|
143
|
+
Validates remote FalkorDB configuration.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple[bool, Optional[str]]: (is_valid, error_message)
|
|
147
|
+
"""
|
|
148
|
+
host = os.getenv('FALKORDB_HOST')
|
|
149
|
+
if not host:
|
|
150
|
+
return False, "FALKORDB_HOST environment variable is not set."
|
|
151
|
+
|
|
152
|
+
port_str = os.getenv('FALKORDB_PORT', '6379')
|
|
153
|
+
try:
|
|
154
|
+
port = int(port_str)
|
|
155
|
+
if not (1 <= port <= 65535):
|
|
156
|
+
return False, f"FALKORDB_PORT must be between 1 and 65535, got {port}."
|
|
157
|
+
except ValueError:
|
|
158
|
+
return False, f"FALKORDB_PORT must be a number, got '{port_str}'."
|
|
159
|
+
|
|
160
|
+
return True, None
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def test_connection() -> Tuple[bool, Optional[str]]:
|
|
164
|
+
"""
|
|
165
|
+
Tests the remote FalkorDB connection.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
from falkordb import FalkorDB
|
|
169
|
+
except ImportError:
|
|
170
|
+
return False, (
|
|
171
|
+
"FalkorDB client is not installed.\n"
|
|
172
|
+
"Install it with: pip install falkordb"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
host = os.getenv('FALKORDB_HOST')
|
|
176
|
+
if not host:
|
|
177
|
+
return False, "FALKORDB_HOST is not set."
|
|
178
|
+
|
|
179
|
+
port = int(os.getenv('FALKORDB_PORT', '6379'))
|
|
180
|
+
password = os.getenv('FALKORDB_PASSWORD') or None
|
|
181
|
+
username = os.getenv('FALKORDB_USERNAME') or None
|
|
182
|
+
ssl = os.getenv('FALKORDB_SSL', 'false').lower() in ('true', '1', 'yes')
|
|
183
|
+
graph_name = os.getenv('FALKORDB_GRAPH_NAME', 'codegraph')
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
kwargs = {'host': host, 'port': port}
|
|
187
|
+
if password:
|
|
188
|
+
kwargs['password'] = password
|
|
189
|
+
if username:
|
|
190
|
+
kwargs['username'] = username
|
|
191
|
+
if ssl:
|
|
192
|
+
kwargs['ssl'] = True
|
|
193
|
+
|
|
194
|
+
db = FalkorDB(**kwargs)
|
|
195
|
+
graph = db.select_graph(graph_name)
|
|
196
|
+
graph.query("RETURN 1")
|
|
197
|
+
return True, None
|
|
198
|
+
except Exception as e:
|
|
199
|
+
return False, f"Connection failed: {e}"
|