Initial upload: Devtoolbelt v1.0.0 - unified CLI toolkit for developers
This commit is contained in:
416
devtoolbelt/commands/database.py
Normal file
416
devtoolbelt/commands/database.py
Normal file
@@ -0,0 +1,416 @@
|
||||
"""Database commands for Devtoolbelt."""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import click
|
||||
from rich import print as rprint
|
||||
from rich.table import Table
|
||||
from sqlalchemy import create_engine, inspect, text
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
from ..config import get_config
|
||||
from ..utils import console
|
||||
|
||||
|
||||
@click.group()
|
||||
def database():
|
||||
"""Database inspection and query commands."""
|
||||
pass
|
||||
|
||||
|
||||
@database.command("list")
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
def list_databases(config: Optional[str]):
|
||||
"""List configured databases."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if not databases:
|
||||
rprint("[yellow]No databases configured.[/yellow]")
|
||||
rprint("Add databases to your config file or use 'db connect' command.")
|
||||
return
|
||||
|
||||
table = Table(title="Configured Databases")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Type", style="green")
|
||||
table.add_column("Host", style="magenta")
|
||||
table.add_column("Database", style="yellow")
|
||||
|
||||
for name, db_config in databases.items():
|
||||
db_type = db_config.get("type", "unknown")
|
||||
host = db_config.get("host", "localhost")
|
||||
db_name = db_config.get("database", name)
|
||||
table.add_row(name, db_type, host, db_name)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
@database.command("connect")
|
||||
@click.argument("name")
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
def connect_db(name: str, config: Optional[str]):
|
||||
"""Connect to a database and show basic info."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if name not in databases:
|
||||
rprint(f"[red]Database '{name}' not found in configuration.[/red]")
|
||||
return
|
||||
|
||||
db_config = databases[name]
|
||||
conn_str = _build_connection_string(db_config)
|
||||
|
||||
try:
|
||||
engine = create_engine(conn_str)
|
||||
with engine.connect():
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
db_name = db_config.get("database", name)
|
||||
|
||||
info_table = Table(title=f"Database: {name}")
|
||||
info_table.add_column("Property", style="cyan")
|
||||
info_table.add_column("Value", style="green")
|
||||
|
||||
info_table.add_row("Database Name", db_name)
|
||||
info_table.add_row("Host", db_config.get("host", "localhost"))
|
||||
info_table.add_row("Port", str(db_config.get("port", "")))
|
||||
info_table.add_row("Tables Count", str(len(tables)))
|
||||
|
||||
console.print(info_table)
|
||||
|
||||
if tables:
|
||||
rprint("\n[bold cyan]Tables:[/bold cyan]")
|
||||
for i, table in enumerate(tables, 1):
|
||||
rprint(f" {i}. {table}")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
rprint(f"[red]Error connecting to database: {e}[/red]")
|
||||
|
||||
|
||||
@database.command("tables")
|
||||
@click.argument("name")
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
@click.option(
|
||||
"--verbose", "-v",
|
||||
is_flag=True,
|
||||
help="Show detailed table information."
|
||||
)
|
||||
def list_tables(name: str, config: Optional[str], verbose: bool):
|
||||
"""List tables in a database."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if name not in databases:
|
||||
rprint(f"[red]Database '{name}' not found in configuration.[/red]")
|
||||
return
|
||||
|
||||
db_config = databases[name]
|
||||
conn_str = _build_connection_string(db_config)
|
||||
|
||||
try:
|
||||
engine = create_engine(conn_str)
|
||||
inspector = inspect(engine)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
if verbose:
|
||||
table = Table(title=f"Tables in {name}")
|
||||
table.add_column("Table Name", style="cyan")
|
||||
table.add_column("Columns", style="green")
|
||||
table.add_column("Type", style="magenta")
|
||||
|
||||
for t in tables:
|
||||
columns = inspector.get_columns(t)
|
||||
table.add_row(
|
||||
t,
|
||||
str(len(columns)),
|
||||
"Table"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
else:
|
||||
rprint(f"[bold]Tables in {name}:[/bold]")
|
||||
for i, table in enumerate(tables, 1):
|
||||
rprint(f" {i}. {table}")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
rprint(f"[red]Error listing tables: {e}[/red]")
|
||||
|
||||
|
||||
@database.command("schema")
|
||||
@click.argument("name")
|
||||
@click.argument("table")
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
def show_schema(name: str, table: str, config: Optional[str]):
|
||||
"""Show schema for a table."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if name not in databases:
|
||||
rprint(f"[red]Database '{name}' not found in configuration.[/red]")
|
||||
return
|
||||
|
||||
db_config = databases[name]
|
||||
conn_str = _build_connection_string(db_config)
|
||||
|
||||
try:
|
||||
engine = create_engine(conn_str)
|
||||
inspector = inspect(engine)
|
||||
|
||||
if table not in inspector.get_table_names():
|
||||
rprint(f"[red]Table '{table}' not found in database.[/red]")
|
||||
return
|
||||
|
||||
columns = inspector.get_columns(table)
|
||||
foreign_keys = inspector.get_foreign_keys(table)
|
||||
indexes = inspector.get_indexes(table)
|
||||
primary_key = inspector.get_pk_constraint(table)
|
||||
|
||||
schema_table = Table(title=f"Schema: {table}")
|
||||
schema_table.add_column("Column", style="cyan")
|
||||
schema_table.add_column("Type", style="green")
|
||||
schema_table.add_column("Nullable", style="magenta")
|
||||
schema_table.add_column("Default", style="yellow")
|
||||
|
||||
for col in columns:
|
||||
col_type = str(col['type'])
|
||||
nullable = "Yes" if col['nullable'] else "No"
|
||||
default = str(col['default']) if col['default'] else "-"
|
||||
schema_table.add_row(
|
||||
col['name'],
|
||||
col_type,
|
||||
nullable,
|
||||
default
|
||||
)
|
||||
|
||||
console.print(schema_table)
|
||||
|
||||
if primary_key.get('constrained_columns'):
|
||||
rprint(f"\n[bold cyan]Primary Key:[/bold cyan] {', '.join(primary_key['constrained_columns'])}")
|
||||
|
||||
if foreign_keys:
|
||||
rprint("\n[bold cyan]Foreign Keys:[/bold cyan]")
|
||||
for fk in foreign_keys:
|
||||
rprint(f" {fk['constrained_columns']} -> {fk['referred_table']}.{fk['referred_columns']}")
|
||||
|
||||
if indexes:
|
||||
rprint("\n[bold cyan]Indexes:[/bold cyan]")
|
||||
for idx in indexes:
|
||||
col_names = [c for c in idx['column_names'] if c is not None]
|
||||
rprint(f" {idx['name']}: {', '.join(col_names)}")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
rprint(f"[red]Error getting schema: {e}[/red]")
|
||||
|
||||
|
||||
@database.command("query")
|
||||
@click.argument("name")
|
||||
@click.argument("query")
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
@click.option(
|
||||
"--limit", "-l",
|
||||
type=int,
|
||||
default=100,
|
||||
help="Maximum number of rows to return."
|
||||
)
|
||||
@click.option(
|
||||
"--format", "-f",
|
||||
type=click.Choice(["table", "json", "csv"]),
|
||||
default="table",
|
||||
help="Output format."
|
||||
)
|
||||
def execute_query(
|
||||
name: str,
|
||||
query: str,
|
||||
config: Optional[str],
|
||||
limit: int,
|
||||
format: str
|
||||
):
|
||||
"""Execute a query on a database."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if name not in databases:
|
||||
rprint(f"[red]Database '{name}' not found in configuration.[/red]")
|
||||
return
|
||||
|
||||
db_config = databases[name]
|
||||
conn_str = _build_connection_string(db_config)
|
||||
|
||||
if limit > 0:
|
||||
if not query.lower().strip().startswith("select"):
|
||||
rprint("[yellow]Warning: LIMIT is only applied to SELECT queries.[/yellow]")
|
||||
else:
|
||||
if "limit" not in query.lower():
|
||||
query = f"{query} LIMIT {limit}"
|
||||
|
||||
try:
|
||||
engine = create_engine(conn_str)
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text(query))
|
||||
columns = result.keys()
|
||||
rows = result.fetchall()
|
||||
|
||||
if format == "json":
|
||||
import json
|
||||
data = [dict(zip(columns, row)) for row in rows]
|
||||
click.echo(json.dumps(data, indent=2, default=str))
|
||||
elif format == "csv":
|
||||
import csv
|
||||
import io
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(columns)
|
||||
writer.writerows(rows)
|
||||
click.echo(output.getvalue())
|
||||
else:
|
||||
table = Table(title=f"Query Results from {name}")
|
||||
for col in columns:
|
||||
table.add_column(str(col), style="green")
|
||||
for row in rows:
|
||||
table.add_row(*[str(cell) for cell in row])
|
||||
console.print(table)
|
||||
|
||||
rprint(f"\n[dim]Rows returned: {len(rows)}[/dim]")
|
||||
|
||||
except SQLAlchemyError as e:
|
||||
rprint(f"[red]Query error: {e}[/red]")
|
||||
|
||||
|
||||
@database.command("add")
|
||||
@click.argument("name")
|
||||
@click.option(
|
||||
"--type", "-t",
|
||||
type=click.Choice(["postgresql", "mysql", "sqlite", "mssql"]),
|
||||
required=True,
|
||||
help="Database type."
|
||||
)
|
||||
@click.option(
|
||||
"--host", "-H",
|
||||
default="localhost",
|
||||
help="Database host."
|
||||
)
|
||||
@click.option(
|
||||
"--port", "-p",
|
||||
type=int,
|
||||
help="Database port."
|
||||
)
|
||||
@click.option(
|
||||
"--database", "-d",
|
||||
required=True,
|
||||
help="Database name."
|
||||
)
|
||||
@click.option(
|
||||
"--user", "-u",
|
||||
help="Database user."
|
||||
)
|
||||
@click.option(
|
||||
"--password", "-P",
|
||||
help="Database password."
|
||||
)
|
||||
@click.option(
|
||||
"--config", "-c",
|
||||
type=click.Path(exists=True),
|
||||
help="Path to configuration file."
|
||||
)
|
||||
def add_database(
|
||||
name: str,
|
||||
type: str,
|
||||
host: str,
|
||||
port: Optional[int],
|
||||
database: str,
|
||||
user: Optional[str],
|
||||
password: Optional[str],
|
||||
config: Optional[str]
|
||||
):
|
||||
"""Add a database configuration."""
|
||||
cfg = get_config(config)
|
||||
databases = cfg.get_database_configs()
|
||||
|
||||
if name in databases:
|
||||
rprint(f"[yellow]Database '{name}' already exists. Overwriting.[/yellow]")
|
||||
|
||||
db_config: Dict[str, Any] = {
|
||||
"type": type,
|
||||
"host": host,
|
||||
"database": database
|
||||
}
|
||||
|
||||
if port:
|
||||
db_config["port"] = port
|
||||
if user:
|
||||
db_config["user"] = user
|
||||
if password:
|
||||
db_config["password"] = password
|
||||
|
||||
databases[name] = db_config
|
||||
cfg.set("databases", databases)
|
||||
cfg.save()
|
||||
|
||||
rprint(f"[green]Database '{name}' added successfully.[/green]")
|
||||
|
||||
|
||||
def _build_connection_string(db_config: Dict[str, Any]) -> str:
|
||||
"""Build SQLAlchemy connection string from config.
|
||||
|
||||
Args:
|
||||
db_config: Database configuration dictionary.
|
||||
|
||||
Returns:
|
||||
Connection string.
|
||||
"""
|
||||
db_type = db_config.get("type", "")
|
||||
|
||||
if db_type == "postgresql":
|
||||
user = db_config.get("user", "")
|
||||
password = db_config.get("password", "")
|
||||
host = db_config.get("host", "localhost")
|
||||
port = db_config.get("port", 5432)
|
||||
database = db_config.get("database", "")
|
||||
if password:
|
||||
return f"postgresql://{user}:{password}@{host}:{port}/{database}"
|
||||
return f"postgresql://{user}@{host}:{port}/{database}"
|
||||
|
||||
elif db_type == "mysql":
|
||||
user = db_config.get("user", "")
|
||||
password = db_config.get("password", "")
|
||||
host = db_config.get("host", "localhost")
|
||||
port = db_config.get("port", 3306)
|
||||
database = db_config.get("database", "")
|
||||
if password:
|
||||
return f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}"
|
||||
return f"mysql+pymysql://{user}@{host}:{port}/{database}"
|
||||
|
||||
elif db_type == "sqlite":
|
||||
database = db_config.get("database", "")
|
||||
return f"sqlite:///{database}"
|
||||
|
||||
elif db_type == "mssql":
|
||||
user = db_config.get("user", "")
|
||||
password = db_config.get("password", "")
|
||||
host = db_config.get("host", "localhost")
|
||||
port = db_config.get("port", 1433)
|
||||
database = db_config.get("database", "")
|
||||
return f"mssql+pyodbc://{user}:{password}@{host}:{port}/{database}?driver=ODBC+Driver+17+for+SQL+Server"
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unsupported database type: {db_type}")
|
||||
Reference in New Issue
Block a user