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