Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ dependencies = [
"python-dotenv>=1.0.0",
"tree-sitter>=0.21.0",
"tree-sitter-language-pack>=0.6.0",
"tree-sitter-c-sharp>=0.21.0",
"tree-sitter-c-sharp>=0.21.2",
"pyyaml",
"nbformat",
"nbconvert>=7.16.6",
Expand All @@ -33,14 +33,15 @@ dependencies = [
"requests>=2.28.0",
"falkordblite>=0.1.0; sys_platform != 'win32' and python_version >= '3.12'",
"fastapi>=0.100.0",
"uvicorn>=0.22.0"
"uvicorn>=0.22.0",
"google-cloud-spanner>=3.40.0"
]

[project.optional-dependencies]
parsing = [
"tree-sitter>=0.21.0",
"tree-sitter-language-pack>=0.6.0",
"tree-sitter-c-sharp>=0.21.0",
"tree-sitter-c-sharp>=0.21.2"
]
dev = [
"pytest>=7.4.0",
Expand Down
6 changes: 3 additions & 3 deletions src/codegraphcontext/cli/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
CONFIG_FILE = CONFIG_DIR / ".env"

# Database credential keys (stored in same .env file but not managed as config)
DATABASE_CREDENTIAL_KEYS = {"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE"}
DATABASE_CREDENTIAL_KEYS = {"NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE", "SPANNER_INSTANCE_ID", "SPANNER_DATABASE_ID", "GOOGLE_CLOUD_PROJECT"}

# Default configuration values
DEFAULT_CONFIG = {
Expand Down Expand Up @@ -48,7 +48,7 @@

# Configuration key descriptions
CONFIG_DESCRIPTIONS = {
"DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb)",
"DEFAULT_DATABASE": "Default database backend (neo4j|falkordb|kuzudb|spanner)",
"FALKORDB_PATH": "Path to FalkorDB database file",
"FALKORDB_SOCKET_PATH": "Path to FalkorDB Unix socket",
"INDEX_VARIABLES": "Index variable nodes in the graph (lighter graph if false)",
Expand All @@ -75,7 +75,7 @@

# Valid values for each config key
CONFIG_VALIDATORS = {
"DEFAULT_DATABASE": ["neo4j", "falkordb", "kuzudb"],
"DEFAULT_DATABASE": ["neo4j", "falkordb", "kuzudb", "spanner"],
"INDEX_VARIABLES": ["true", "false"],
"ALLOW_DB_DELETION": ["true", "false"],
"DEBUG_LOGS": ["true", "false"],
Expand Down
24 changes: 23 additions & 1 deletion src/codegraphcontext/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from codegraphcontext.server import MCPServer
from codegraphcontext.core.database import DatabaseManager
from .setup_wizard import run_neo4j_setup_wizard, configure_mcp_client
from .setup_wizard import run_neo4j_setup_wizard, configure_mcp_client, run_spanner_setup_wizard
from . import config_manager
# Import the new helper functions
from .cli_helpers import (
Expand Down Expand Up @@ -213,6 +213,26 @@ def neo4j_setup_alias():
neo4j_setup()


# Create Spanner command group
spanner_app = typer.Typer(help="Spanner database configuration commands")
app.add_typer(spanner_app, name="spanner")

@spanner_app.command("setup")
def spanner_setup():
"""
Configure Google Cloud Spanner Database Connection.
"""
console.print("\n[bold cyan]Spanner Database Setup[/bold cyan]")
console.print("Configure Spanner database connection for CodeGraphContext.\n")
run_spanner_setup_wizard()

# Abbreviation for spanner setup
@app.command("s", rich_help_panel="Shortcuts")
def spanner_setup_alias():
"""Shortcut for 'cgc spanner setup'"""
spanner_setup()


# ============================================================================
# CREDENTIALS LOADING PRECEDENCE
# ============================================================================
Expand Down Expand Up @@ -344,6 +364,8 @@ def _load_credentials():
console.print("[cyan]Using database: FalkorDB Lite[/cyan]")
elif default_db == "kuzudb":
console.print("[cyan]Using database: KùzuDB[/cyan]")
elif default_db == "spanner":
console.print(f"[cyan]Using database: Spanner (project: {os.environ.get('GOOGLE_CLOUD_PROJECT', 'default')}, instance: {os.environ.get('SPANNER_INSTANCE_ID', 'default')}, database: {os.environ.get('SPANNER_DATABASE_ID', 'default')})[/cyan]")
elif default_db == "falkordb-remote":
host = os.environ.get("FALKORDB_HOST")
if host:
Expand Down
80 changes: 80 additions & 0 deletions src/codegraphcontext/cli/setup_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -968,3 +968,83 @@ def setup_local_binary():
}
_save_neo4j_credentials(creds)
console.print("\n[bold green]All done! Your local Neo4j instance is ready to use.[/bold green]")


def _save_spanner_credentials(creds):
"""
Save Spanner credentials to .env file (database setup only).
"""
from codegraphcontext.cli.config_manager import load_config, save_config, ensure_config_dir

ensure_config_dir()

config = load_config()

config["SPANNER_INSTANCE_ID"] = creds.get('instance_id', 'infrastream-core')
config["SPANNER_DATABASE_ID"] = creds.get('database_id', 'infrastream-state')
config["GOOGLE_CLOUD_PROJECT"] = creds.get('project', '')

config["DEFAULT_DATABASE"] = "spanner"

save_config(config, preserve_db_credentials=False)

console.print("\n[bold green]✅ Spanner setup complete![/bold green]")
console.print(f"[cyan]📝 Credentials saved to ~/.codegraphcontext/.env[/cyan]")
console.print(f"[cyan]🔧 Default database set to 'spanner'[/cyan]")
console.print("\n[dim]You can now use cgc commands with Spanner:[/dim]")
console.print("[dim] • cgc index . - Index your code[/dim]")
console.print("[dim] • cgc find function - Search your codebase[/dim]")


def run_spanner_setup_wizard():
"""Guides the user through setting up Spanner database for CodeGraphContext."""
console.print("\n[bold cyan]Welcome to the Spanner Setup Wizard![/bold cyan]")
console.print("Please enter your Google Cloud Spanner connection details.")
console.print("[dim]CodeGraphContext will use these details to connect to Google Cloud Spanner.[/dim]\n")

while True:
questions = [
{"type": "input", "message": "Google Cloud Project ID:", "name": "project"},
{"type": "input", "message": "Spanner Instance ID:", "name": "instance_id", "default": "my-spanner-instance"},
{"type": "input", "message": "Spanner Database ID:", "name": "database_id", "default": "my-graph-database"},
]

creds = prompt(questions)
if not creds:
return # User cancelled

console.print("\n[cyan]🔗 Validating configuration locally...[/cyan]")

# Test Spanner via environmental mock
old_proj = os.environ.get("GOOGLE_CLOUD_PROJECT")
old_inst = os.environ.get("SPANNER_INSTANCE_ID")
old_db = os.environ.get("SPANNER_DATABASE_ID")

os.environ["GOOGLE_CLOUD_PROJECT"] = creds.get("project", "")
os.environ["SPANNER_INSTANCE_ID"] = creds.get("instance_id", "")
os.environ["SPANNER_DATABASE_ID"] = creds.get("database_id", "")

try:
from codegraphcontext.core.database_spanner import SpannerDBManager
mgr = SpannerDBManager()

console.print("\n[cyan]🔗 Testing connection (requires active gcloud auth)...[/cyan]")
mgr.get_driver() # Check connection

console.print("[green]✅ Connection successful![/green]")
mgr.provision_schema(console)
_save_spanner_credentials(creds)
break
except Exception as e:
console.print(f"[red]❌ Connection failed: {e}[/red]")
console.print("[dim]Please make sure you have authenticated using exactly 'gcloud auth application-default login' on this terminal.[/dim]")
retry = prompt([{"type": "confirm", "message": "Try again?", "name": "retry", "default": True}])
if not retry.get("retry"):
break
finally:
if old_proj is not None: os.environ["GOOGLE_CLOUD_PROJECT"] = old_proj
elif "GOOGLE_CLOUD_PROJECT" in os.environ: del os.environ["GOOGLE_CLOUD_PROJECT"]
if old_inst is not None: os.environ["SPANNER_INSTANCE_ID"] = old_inst
elif "SPANNER_INSTANCE_ID" in os.environ: del os.environ["SPANNER_INSTANCE_ID"]
if old_db is not None: os.environ["SPANNER_DATABASE_ID"] = old_db
elif "SPANNER_DATABASE_ID" in os.environ: del os.environ["SPANNER_DATABASE_ID"]
25 changes: 21 additions & 4 deletions src/codegraphcontext/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ def _is_neo4j_configured() -> bool:
os.getenv('NEO4J_PASSWORD')
])

def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager']:
def _is_spanner_configured() -> bool:
"""Check if Google Cloud Spanner is configured via environment variables."""
return bool(os.getenv('SPANNER_INSTANCE_ID') and os.getenv('SPANNER_DATABASE_ID'))

def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'SpannerDBManager']:
"""
Factory function to get the appropriate database manager based on configuration.

Expand Down Expand Up @@ -122,8 +126,14 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
from .database import DatabaseManager
info_logger("Using Neo4j Server (explicit)")
return DatabaseManager()
elif db_type == 'spanner':
if not _is_spanner_configured():
raise ValueError("Database set to 'spanner' but it is not configured.\nSet SPANNER_INSTANCE_ID and SPANNER_DATABASE_ID.")
from .database_spanner import SpannerDBManager
info_logger("Using Google Cloud Spanner Server (explicit)")
return SpannerDBManager()
else:
raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', or 'neo4j'.")
raise ValueError(f"Unknown database type: '{db_type}'. Use 'kuzudb', 'falkordb', 'falkordb-remote', 'neo4j', or 'spanner'.")

# 4. Auto-detect: Remote FalkorDB (if FALKORDB_HOST is set)
# This takes priority over zero-config local backends because it's an explicit signal
Expand Down Expand Up @@ -152,7 +162,13 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
info_logger("Using KùzuDB (default)")
return KuzuDBManager()

# 7. Fallback if configured
# 7. Fallback if configured -> Spanner
if _is_spanner_configured():
from .database_spanner import SpannerDBManager
info_logger("Using Google Cloud Spanner Server (auto-detected)")
return SpannerDBManager()

# 8. Fallback if configured -> Neo4j
if _is_neo4j_configured():
from .database import DatabaseManager
info_logger("Using Neo4j Server (auto-detected)")
Expand All @@ -173,5 +189,6 @@ def get_database_manager() -> Union['DatabaseManager', 'FalkorDBManager', 'Falko
from .database_falkordb import FalkorDBManager
from .database_falkordb_remote import FalkorDBRemoteManager
from .database_kuzu import KuzuDBManager
from .database_spanner import SpannerDBManager

__all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'get_database_manager']
__all__ = ['DatabaseManager', 'FalkorDBManager', 'FalkorDBRemoteManager', 'KuzuDBManager', 'SpannerDBManager', 'get_database_manager']
Loading