Skip to content

Commit 56995cf

Browse files
committed
Add --connection flag support to CLI commands
- Add --connection flag to workbench, query, and serve commands - Support all database connection strings (postgres, bigquery, snowflake, clickhouse, databricks, spark) - Keep backward compatible --db flag for DuckDB files - Update command docstrings with connection examples - 573 tests passing
1 parent aa6bba9 commit 56995cf

1 file changed

Lines changed: 63 additions & 30 deletions

File tree

sidemantic/cli.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ def main(
5757
def workbench(
5858
directory: Path = typer.Argument(".", help="Directory containing semantic layer files (defaults to current dir)"),
5959
demo: bool = typer.Option(False, "--demo", help="Launch with demo data (multi-format example)"),
60-
db: Path = typer.Option(None, "--db", help="Path to DuckDB database file (optional)"),
60+
connection: str = typer.Option(
61+
None, "--connection", help="Database connection string (e.g., postgres://host/db, bigquery://project/dataset)"
62+
),
63+
db: Path = typer.Option(None, "--db", help="Path to DuckDB database file (shorthand for duckdb:/// connection)"),
6164
):
6265
"""
6366
Interactive semantic layer workbench with SQL editor and charting.
@@ -67,7 +70,8 @@ def workbench(
6770
Examples:
6871
sidemantic workbench semantic_models/ # Your own models
6972
sidemantic workbench --demo # Try the demo
70-
sidemantic workbench ./models --db data/warehouse.db # With database
73+
sidemantic workbench ./models --db data/warehouse.db # With DuckDB
74+
sidemantic workbench ./models --connection "postgres://localhost:5432/db"
7175
uvx sidemantic workbench --demo # Run demo without installing
7276
"""
7377
from sidemantic.workbench import run_workbench
@@ -97,17 +101,18 @@ def workbench(
97101
raise typer.Exit(1)
98102
else:
99103
# Build connection string from args or config
100-
if db:
104+
connection_str = None
105+
if connection:
106+
# Explicit --connection arg provided
107+
connection_str = connection
108+
elif db:
101109
# Explicit --db arg provided
102-
connection = f"duckdb:///{db.absolute()}"
110+
connection_str = f"duckdb:///{db.absolute()}"
103111
elif _loaded_config and _loaded_config.connection:
104112
# Use connection from config
105-
connection = build_connection_string(_loaded_config)
106-
else:
107-
# Default behavior
108-
connection = None
113+
connection_str = build_connection_string(_loaded_config)
109114

110-
run_workbench(directory, connection=connection)
115+
run_workbench(directory, connection=connection_str)
111116

112117

113118
@app.command()
@@ -150,27 +155,46 @@ def query(
150155
directory: Path = typer.Argument(..., help="Directory containing semantic layer files"),
151156
sql: str = typer.Option(..., "--sql", "-q", help="SQL query to execute"),
152157
output: Path = typer.Option(None, "--output", "-o", help="Output file (default: stdout)"),
158+
connection: str = typer.Option(
159+
None, "--connection", help="Database connection string (e.g., postgres://host/db, bigquery://project/dataset)"
160+
),
161+
db: Path = typer.Option(None, "--db", help="Path to DuckDB database file (shorthand for duckdb:/// connection)"),
153162
):
154163
"""
155164
Execute a SQL query and output results as CSV.
156165
157-
Example: sidemantic query /path/to/models --sql "SELECT orders.revenue FROM orders"
166+
Examples:
167+
sidemantic query models/ --sql "SELECT revenue FROM orders"
168+
sidemantic query models/ --sql "SELECT * FROM orders" --output results.csv
169+
sidemantic query models/ --connection "postgres://localhost:5432/db" --sql "SELECT revenue FROM orders"
170+
sidemantic query models/ --db data.duckdb --sql "SELECT revenue FROM orders"
158171
"""
159172
if not directory.exists():
160173
typer.echo(f"Error: Directory {directory} does not exist", err=True)
161174
raise typer.Exit(1)
162175

163176
try:
164-
# Try to find database file
165-
db_path = None
166-
data_dir = directory / "data"
167-
if data_dir.exists():
168-
db_files = list(data_dir.glob("*.db"))
169-
if db_files:
170-
db_path = f"duckdb:///{db_files[0].absolute()}"
177+
# Build connection string from args or config
178+
connection_str = None
179+
if connection:
180+
# Explicit --connection arg provided
181+
connection_str = connection
182+
elif db:
183+
# Explicit --db arg provided
184+
connection_str = f"duckdb:///{db.absolute()}"
185+
elif _loaded_config and _loaded_config.connection:
186+
# Use connection from config
187+
connection_str = build_connection_string(_loaded_config)
188+
else:
189+
# Try to find database file in data/
190+
data_dir = directory / "data"
191+
if data_dir.exists():
192+
db_files = list(data_dir.glob("*.db"))
193+
if db_files:
194+
connection_str = f"duckdb:///{db_files[0].absolute()}"
171195

172196
# Load semantic layer
173-
layer = SemanticLayer(connection=db_path)
197+
layer = SemanticLayer(connection=connection_str)
174198
load_from_directory(layer, str(directory))
175199

176200
if not layer.graph.models:
@@ -352,7 +376,10 @@ def mcp_serve(
352376
def serve(
353377
directory: Path = typer.Argument(".", help="Directory containing semantic layer files (defaults to current dir)"),
354378
demo: bool = typer.Option(False, "--demo", help="Use demo data"),
355-
db: Path = typer.Option(None, "--db", help="Path to DuckDB database file (optional)"),
379+
connection: str = typer.Option(
380+
None, "--connection", help="Database connection string (e.g., postgres://host/db, bigquery://project/dataset)"
381+
),
382+
db: Path = typer.Option(None, "--db", help="Path to DuckDB database file (shorthand for duckdb:/// connection)"),
356383
port: int = typer.Option(None, "--port", "-p", help="Port to listen on (overrides config)"),
357384
username: str = typer.Option(None, "--username", "-u", help="Username for authentication (overrides config)"),
358385
password: str = typer.Option(None, "--password", help="Password for authentication (overrides config)"),
@@ -366,6 +393,8 @@ def serve(
366393
Examples:
367394
sidemantic serve ./models --port 5433
368395
sidemantic serve ./models --db data/warehouse.db
396+
sidemantic serve ./models --connection "postgres://localhost:5432/analytics"
397+
sidemantic serve ./models --connection "bigquery://project/dataset" --port 5433
369398
sidemantic serve --demo
370399
sidemantic serve ./models --username user --password secret
371400
"""
@@ -399,24 +428,28 @@ def serve(
399428
typer.echo(f"Error: Directory {directory} does not exist", err=True)
400429
raise typer.Exit(1)
401430

402-
# Resolve db from args or config
403-
db_resolved = None
404-
if db:
405-
db_resolved = db
431+
# Build connection string from args or config
432+
connection_str = None
433+
if connection:
434+
# Explicit --connection arg provided
435+
connection_str = connection
436+
elif db:
437+
# Explicit --db arg provided
438+
connection_str = f"duckdb:///{db.absolute()}"
406439
elif _loaded_config and _loaded_config.connection:
407-
from sidemantic.config import DuckDBConnection
408-
409-
if isinstance(_loaded_config.connection, DuckDBConnection):
410-
db_resolved = Path(_loaded_config.connection.path) if _loaded_config.connection.path != ":memory:" else None
440+
# Use connection from config
441+
connection_str = build_connection_string(_loaded_config)
442+
else:
443+
# Default to in-memory DuckDB
444+
connection_str = "duckdb:///:memory:"
411445

412446
# Resolve port, username, password from args or config
413447
port_resolved = port if port is not None else (_loaded_config.pg_server.port if _loaded_config else 5433)
414448
username_resolved = username or (_loaded_config.pg_server.username if _loaded_config else None)
415449
password_resolved = password or (_loaded_config.pg_server.password if _loaded_config else None)
416450

417-
# Create semantic layer with explicit db or in-memory
418-
db_path = f"duckdb:///{Path(db_resolved).absolute()}" if db_resolved else "duckdb:///:memory:"
419-
layer = SemanticLayer(connection=db_path)
451+
# Create semantic layer with connection string
452+
layer = SemanticLayer(connection=connection_str)
420453

421454
# Load models
422455
load_from_directory(layer, str(directory))

0 commit comments

Comments
 (0)