dbagent-cli 0.1.0__py3-none-any.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.
@@ -0,0 +1,145 @@
1
+ """
2
+ Local Ollama LLM provider (100% Free, Offline, Zero-cost).
3
+ Auto-installs and configures Ollama on first use — fully independent.
4
+ """
5
+
6
+ import json
7
+ import requests
8
+ from typing import Optional, Callable, List
9
+ from dbagent.llm.base import BaseLLMProvider
10
+
11
+
12
+ class OllamaProvider(BaseLLMProvider):
13
+ """Local offline provider using Ollama. Auto-sets up on first use."""
14
+
15
+ def __init__(self, base_url: str = "http://localhost:11434", default_model: str = "qwen2.5-coder:1.5b"):
16
+ self.base_url = base_url.rstrip("/")
17
+ self.default_model = default_model
18
+ self._setup_done = False
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return "ollama"
23
+
24
+ def is_available(self) -> bool:
25
+ """Check if local Ollama daemon is running."""
26
+ try:
27
+ r = requests.get(f"{self.base_url}/api/tags", timeout=0.5)
28
+ return r.status_code == 200
29
+ except Exception:
30
+ return False
31
+
32
+ def list_models(self) -> List[str]:
33
+ """List all installed local models."""
34
+ try:
35
+ r = requests.get(f"{self.base_url}/api/tags", timeout=3)
36
+ if r.status_code == 200:
37
+ data = r.json()
38
+ return [m.get("name") for m in data.get("models", [])]
39
+ except Exception:
40
+ pass
41
+ return []
42
+
43
+ def _ensure_ready(self, print_fn=None) -> None:
44
+ """Auto-install, start, and pull model if needed. Called once per session."""
45
+ if self._setup_done and self.is_available():
46
+ return
47
+
48
+ from dbagent.llm.auto_setup import ensure_ollama_ready, get_best_available_model
49
+
50
+ success, msg = ensure_ollama_ready(
51
+ model=self.default_model,
52
+ print_fn=print_fn,
53
+ )
54
+
55
+ if not success:
56
+ raise ConnectionError(msg)
57
+
58
+ # Use the best available model
59
+ best = get_best_available_model()
60
+ if best:
61
+ self.default_model = best
62
+
63
+ self._setup_done = True
64
+
65
+ def generate(
66
+ self,
67
+ prompt: str,
68
+ system_prompt: Optional[str] = None,
69
+ model: Optional[str] = None,
70
+ stream_callback: Optional[Callable[[str], None]] = None,
71
+ ) -> str:
72
+ # Auto-setup on first generate call
73
+ if not self.is_available():
74
+ # Build a print function that uses Rich console if available
75
+ def _print_fn(msg, level="info"):
76
+ try:
77
+ from dbagent.ui.console import print_info, print_success, print_warning, print_error
78
+ if level == "success":
79
+ print_success(msg)
80
+ elif level == "warning":
81
+ print_warning(msg)
82
+ elif level == "error":
83
+ print_error(msg)
84
+ else:
85
+ print_info(msg)
86
+ except Exception:
87
+ print(f"[{level.upper()}] {msg}")
88
+
89
+ self._ensure_ready(print_fn=_print_fn)
90
+
91
+ target_model = model or self.default_model
92
+
93
+ # Check if requested model exists locally, pick best available
94
+ available = self.list_models()
95
+ if available and target_model not in available:
96
+ # Try partial match
97
+ for m in available:
98
+ if target_model.split(":")[0] in m:
99
+ target_model = m
100
+ break
101
+ else:
102
+ target_model = available[0]
103
+
104
+ payload = {
105
+ "model": target_model,
106
+ "prompt": prompt,
107
+ "system": system_prompt or "You are an expert database AI engineer.",
108
+ "stream": stream_callback is not None,
109
+ "options": {
110
+ "temperature": 0.1,
111
+ },
112
+ }
113
+
114
+ try:
115
+ if stream_callback:
116
+ response = requests.post(
117
+ f"{self.base_url}/api/generate",
118
+ json=payload,
119
+ stream=True,
120
+ timeout=120,
121
+ )
122
+ full_text = []
123
+ for line in response.iter_lines():
124
+ if line:
125
+ chunk = json.loads(line)
126
+ text_part = chunk.get("response", "")
127
+ full_text.append(text_part)
128
+ stream_callback(text_part)
129
+ return "".join(full_text)
130
+ else:
131
+ response = requests.post(
132
+ f"{self.base_url}/api/generate",
133
+ json=payload,
134
+ timeout=120,
135
+ )
136
+ if response.status_code == 200:
137
+ return response.json().get("response", "")
138
+ else:
139
+ raise RuntimeError(f"Ollama error ({response.status_code}): {response.text}")
140
+ except requests.exceptions.ConnectionError:
141
+ raise ConnectionError(
142
+ "Could not connect to Ollama. Try running `ollama serve` manually."
143
+ )
144
+ except Exception as e:
145
+ raise RuntimeError(f"Ollama generation failed: {str(e)}")
@@ -0,0 +1,100 @@
1
+ """
2
+ OpenRouter Provider for free tier models (e.g., Llama 3.3 70B, Qwen 2.5 Coder 32B Free).
3
+ """
4
+
5
+ import json
6
+ import os
7
+ import requests
8
+ from typing import Optional, Callable, List
9
+ from dbagent.llm.base import BaseLLMProvider
10
+
11
+
12
+ class OpenRouterProvider(BaseLLMProvider):
13
+ """OpenRouter Cloud Provider with access to free-tier open models."""
14
+
15
+ def __init__(self, api_key: Optional[str] = None, default_model: str = "meta-llama/llama-3.3-70b-instruct:free"):
16
+ self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
17
+ self.default_model = default_model
18
+ self.endpoint = "https://openrouter.ai/api/v1/chat/completions"
19
+
20
+ @property
21
+ def name(self) -> str:
22
+ return "openrouter"
23
+
24
+ def is_available(self) -> bool:
25
+ return bool(self.api_key)
26
+
27
+ def list_models(self) -> List[str]:
28
+ return [
29
+ "meta-llama/llama-3.3-70b-instruct:free",
30
+ "qwen/qwen-2.5-coder-32b-instruct:free",
31
+ "mistralai/mistral-7b-instruct:free",
32
+ "google/gemini-2.0-flash-exp:free",
33
+ ]
34
+
35
+ def generate(
36
+ self,
37
+ prompt: str,
38
+ system_prompt: Optional[str] = None,
39
+ model: Optional[str] = None,
40
+ stream_callback: Optional[Callable[[str], None]] = None,
41
+ ) -> str:
42
+ if not self.api_key:
43
+ raise ValueError(
44
+ "OpenRouter API key is not configured. Set OPENROUTER_API_KEY environment variable or run `db-agent config`."
45
+ )
46
+
47
+ target_model = model or self.default_model
48
+
49
+ headers = {
50
+ "Authorization": f"Bearer {self.api_key}",
51
+ "HTTP-Referer": "https://github.com/db-agent/db-agent",
52
+ "X-Title": "DB-Agent",
53
+ "Content-Type": "application/json",
54
+ }
55
+
56
+ messages = []
57
+ if system_prompt:
58
+ messages.append({"role": "system", "content": system_prompt})
59
+ messages.append({"role": "user", "content": prompt})
60
+
61
+ payload = {
62
+ "model": target_model,
63
+ "messages": messages,
64
+ "temperature": 0.1,
65
+ "stream": stream_callback is not None,
66
+ }
67
+
68
+ try:
69
+ if stream_callback:
70
+ response = requests.post(self.endpoint, headers=headers, json=payload, stream=True, timeout=60)
71
+ if response.status_code != 200:
72
+ raise RuntimeError(f"OpenRouter API error ({response.status_code}): {response.text}")
73
+
74
+ full_text = []
75
+ for line in response.iter_lines():
76
+ if line:
77
+ decoded = line.decode("utf-8")
78
+ if decoded.startswith("data: "):
79
+ raw_json = decoded[6:].strip()
80
+ if raw_json == "[DONE]":
81
+ break
82
+ try:
83
+ chunk = json.loads(raw_json)
84
+ delta = chunk.get("choices", [{}])[0].get("delta", {})
85
+ text_part = delta.get("content", "")
86
+ if text_part:
87
+ full_text.append(text_part)
88
+ stream_callback(text_part)
89
+ except Exception:
90
+ pass
91
+ return "".join(full_text)
92
+ else:
93
+ response = requests.post(self.endpoint, headers=headers, json=payload, timeout=60)
94
+ if response.status_code == 200:
95
+ data = response.json()
96
+ return data.get("choices", [{}])[0].get("message", {}).get("content", "")
97
+ else:
98
+ raise RuntimeError(f"OpenRouter API error ({response.status_code}): {response.text}")
99
+ except Exception as e:
100
+ raise RuntimeError(f"OpenRouter generation error: {str(e)}")
@@ -0,0 +1,123 @@
1
+ """
2
+ Schema formatters for converting database metadata into rich LLM context.
3
+ """
4
+
5
+ from typing import List, Optional
6
+ from dbagent.schema.models import DatabaseSchema, TableModel
7
+
8
+
9
+ class SchemaFormatter:
10
+ """Formats DatabaseSchema into prompts for LLMs."""
11
+
12
+ @staticmethod
13
+ def to_markdown(
14
+ schema: DatabaseSchema,
15
+ selected_tables: Optional[List[str]] = None,
16
+ include_samples: bool = True,
17
+ max_sample_rows: int = 2,
18
+ ) -> str:
19
+ """Render schema into a comprehensive markdown description."""
20
+ lines = []
21
+ lines.append(f"# Database Catalog: {schema.database_name}")
22
+ lines.append(f"- **Dialect**: `{schema.dialect_name}`")
23
+ if schema.server_version:
24
+ lines.append(f"- **Server Version**: {schema.server_version}")
25
+ lines.append(f"- **Total Tables**: {schema.total_tables}")
26
+ lines.append(f"- **Total Views**: {schema.total_views}")
27
+ lines.append("")
28
+
29
+ tables_to_include = schema.tables
30
+ if selected_tables is not None:
31
+ lower_selected = [t.lower() for t in selected_tables]
32
+ tables_to_include = [t for t in schema.tables if t.name.lower() in lower_selected]
33
+
34
+ for table in tables_to_include:
35
+ table_type = "View" if table.is_view else "Table"
36
+ lines.append(f"## {table_type}: `{table.name}`")
37
+ if table.comment:
38
+ lines.append(f"*{table.comment}*")
39
+ if table.row_count is not None:
40
+ lines.append(f"- Approximate Row Count: {table.row_count:,}")
41
+ lines.append("")
42
+
43
+ # Columns table
44
+ lines.append("| Column | Type | Nullable | PK | Default | Comment |")
45
+ lines.append("|---|---|:---:|:---:|---|---|")
46
+ for col in table.columns:
47
+ pk_mark = "✅ PK" if col.is_primary_key else ""
48
+ null_mark = "NULL" if col.is_nullable else "NOT NULL"
49
+ default_val = f"`{col.default_value}`" if col.default_value else ""
50
+ comment = col.comment or ""
51
+ lines.append(
52
+ f"| `{col.name}` | `{col.data_type}` | {null_mark} | {pk_mark} | {default_val} | {comment} |"
53
+ )
54
+ lines.append("")
55
+
56
+ # Foreign Keys
57
+ if table.foreign_keys:
58
+ lines.append("### Foreign Key Relationships")
59
+ for fk in table.foreign_keys:
60
+ cols = ", ".join(f"`{c}`" for c in fk.constrained_columns)
61
+ ref_cols = ", ".join(f"`{c}`" for c in fk.referred_columns)
62
+ lines.append(f"- ({cols}) ➡️ `{fk.referred_table}`({ref_cols})")
63
+ lines.append("")
64
+
65
+ # Indexes
66
+ if table.indexes:
67
+ lines.append("### Indexes")
68
+ for idx in table.indexes:
69
+ idx_type = "UNIQUE " if idx.is_unique else ""
70
+ cols = ", ".join(f"`{c}`" for c in idx.columns)
71
+ lines.append(f"- {idx_type}INDEX `{idx.name}` ({cols})")
72
+ lines.append("")
73
+
74
+ # View definition
75
+ if table.is_view and table.view_definition:
76
+ lines.append("### View Definition")
77
+ lines.append("```sql")
78
+ lines.append(table.view_definition.strip())
79
+ lines.append("```")
80
+ lines.append("")
81
+
82
+ # Sample Rows
83
+ if include_samples and table.sample_rows:
84
+ lines.append("### Sample Data (Shape & Types)")
85
+ samples = table.sample_rows[:max_sample_rows]
86
+ if samples:
87
+ headers = list(samples[0].keys())
88
+ lines.append("| " + " | ".join(headers) + " |")
89
+ lines.append("| " + " | ".join(["---"] * len(headers)) + " |")
90
+ for row in samples:
91
+ row_vals = []
92
+ for h in headers:
93
+ val = row.get(h)
94
+ if val is None:
95
+ row_vals.append("`NULL`")
96
+ else:
97
+ val_str = str(val).replace("\n", " ")
98
+ if len(val_str) > 30:
99
+ val_str = val_str[:27] + "..."
100
+ row_vals.append(f"`{val_str}`")
101
+ lines.append("| " + " | ".join(row_vals) + " |")
102
+ lines.append("")
103
+
104
+ lines.append("---")
105
+ lines.append("")
106
+
107
+ return "\n".join(lines)
108
+
109
+ @staticmethod
110
+ def to_compact(schema: DatabaseSchema) -> str:
111
+ """Render a compact single-line per table summary (for huge databases)."""
112
+ lines = [f"Database: {schema.database_name} ({schema.dialect_name})"]
113
+ for t in schema.tables:
114
+ cols = []
115
+ for c in t.columns:
116
+ pk = " [PK]" if c.is_primary_key else ""
117
+ cols.append(f"{c.name}: {c.data_type}{pk}")
118
+ fks = []
119
+ for fk in t.foreign_keys:
120
+ fks.append(f"{','.join(fk.constrained_columns)}->{fk.referred_table}({','.join(fk.referred_columns)})")
121
+ fk_str = f" | FK: {'; '.join(fks)}" if fks else ""
122
+ lines.append(f"- {t.name} ({', '.join(cols)}){fk_str}")
123
+ return "\n".join(lines)
@@ -0,0 +1,72 @@
1
+ """
2
+ Schema catalog models for structured representation of databases.
3
+ """
4
+
5
+ from typing import List, Dict, Any, Optional
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class ColumnModel(BaseModel):
10
+ """Represents a database table column."""
11
+ name: str
12
+ data_type: str
13
+ is_nullable: bool = True
14
+ is_primary_key: bool = False
15
+ default_value: Optional[str] = None
16
+ comment: Optional[str] = None
17
+ is_autoincrement: bool = False
18
+
19
+
20
+ class ForeignKeyModel(BaseModel):
21
+ """Represents a foreign key constraint."""
22
+ name: Optional[str] = None
23
+ constrained_columns: List[str]
24
+ referred_table: str
25
+ referred_columns: List[str]
26
+
27
+
28
+ class IndexModel(BaseModel):
29
+ """Represents a database index."""
30
+ name: str
31
+ columns: List[str]
32
+ is_unique: bool = False
33
+
34
+
35
+ class TableModel(BaseModel):
36
+ """Represents a database table or view."""
37
+ name: str
38
+ schema_name: Optional[str] = None
39
+ is_view: bool = False
40
+ columns: List[ColumnModel] = Field(default_factory=list)
41
+ primary_key: List[str] = Field(default_factory=list)
42
+ foreign_keys: List[ForeignKeyModel] = Field(default_factory=list)
43
+ indexes: List[IndexModel] = Field(default_factory=list)
44
+ row_count: Optional[int] = None
45
+ sample_rows: List[Dict[str, Any]] = Field(default_factory=list)
46
+ view_definition: Optional[str] = None
47
+ comment: Optional[str] = None
48
+
49
+
50
+ class DatabaseSchema(BaseModel):
51
+ """Complete catalog of an introspected database."""
52
+ dialect_name: str
53
+ database_name: str
54
+ server_version: Optional[str] = None
55
+ tables: List[TableModel] = Field(default_factory=list)
56
+
57
+ @property
58
+ def total_tables(self) -> int:
59
+ return len([t for t in self.tables if not t.is_view])
60
+
61
+ @property
62
+ def total_views(self) -> int:
63
+ return len([t for t in self.tables if t.is_view])
64
+
65
+ def get_table(self, table_name: str) -> Optional[TableModel]:
66
+ for t in self.tables:
67
+ if t.name.lower() == table_name.lower():
68
+ return t
69
+ return None
70
+
71
+ def get_table_names(self) -> List[str]:
72
+ return [t.name for t in self.tables]
@@ -0,0 +1,70 @@
1
+ """
2
+ Smart schema selector for filtering relevant tables in large databases.
3
+ """
4
+
5
+ import re
6
+ from typing import List, Set
7
+ from dbagent.schema.models import DatabaseSchema, TableModel
8
+
9
+
10
+ class SchemaSelector:
11
+ """Selects subset of relevant tables from a large schema based on a user prompt."""
12
+
13
+ @classmethod
14
+ def select_relevant_tables(
15
+ cls,
16
+ schema: DatabaseSchema,
17
+ user_prompt: str,
18
+ max_tables: int = 15,
19
+ ) -> List[str]:
20
+ """
21
+ Extract relevant table names from user query and include connected foreign key tables.
22
+ If total tables in database <= max_tables, returns all tables.
23
+ """
24
+ all_table_names = [t.name for t in schema.tables]
25
+ if len(all_table_names) <= max_tables:
26
+ return all_table_names
27
+
28
+ prompt_lower = user_prompt.lower()
29
+ matched_tables: Set[str] = set()
30
+
31
+ # 1. Direct match or word boundary match
32
+ for table in schema.tables:
33
+ t_name = table.name.lower()
34
+ # Check exact word or substring
35
+ pattern = r"\b" + re.escape(t_name) + r"\b"
36
+ if re.search(pattern, prompt_lower) or t_name in prompt_lower:
37
+ matched_tables.add(table.name)
38
+ continue
39
+
40
+ # Check if any column name is specifically mentioned
41
+ for col in table.columns:
42
+ c_name = col.name.lower()
43
+ if len(c_name) > 3 and re.search(r"\b" + re.escape(c_name) + r"\b", prompt_lower):
44
+ matched_tables.add(table.name)
45
+ break
46
+
47
+ # 2. Add foreign-key related tables for matched tables
48
+ related_tables: Set[str] = set()
49
+ for t_name in matched_tables:
50
+ t_model = schema.get_table(t_name)
51
+ if t_model:
52
+ for fk in t_model.foreign_keys:
53
+ if fk.referred_table in all_table_names:
54
+ related_tables.add(fk.referred_table)
55
+
56
+ # Also find any other table that references the matched table
57
+ for table in schema.tables:
58
+ for fk in table.foreign_keys:
59
+ if fk.referred_table in matched_tables:
60
+ related_tables.add(table.name)
61
+
62
+ combined = matched_tables.union(related_tables)
63
+
64
+ # If nothing matched (e.g. general prompt like "list everything"), return top tables
65
+ if not combined:
66
+ return all_table_names[:max_tables]
67
+
68
+ # Convert to list capped at max_tables
69
+ result = list(combined)[:max_tables]
70
+ return result
dbagent/ui/console.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Rich terminal UI utilities and console formatting.
3
+ """
4
+
5
+ import sys
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+ from rich.syntax import Syntax
9
+ from rich.text import Text
10
+ from rich.table import Table
11
+ from typing import Optional, List, Dict, Any
12
+
13
+ # Ensure UTF-8 on Windows terminal streams
14
+ if sys.platform == "win32":
15
+ try:
16
+ if hasattr(sys.stdout, "reconfigure"):
17
+ sys.stdout.reconfigure(encoding="utf-8")
18
+ if hasattr(sys.stderr, "reconfigure"):
19
+ sys.stderr.reconfigure(encoding="utf-8")
20
+ except Exception:
21
+ pass
22
+
23
+ console = Console()
24
+
25
+
26
+ def print_banner() -> None:
27
+ """Print the DB-Agent terminal banner."""
28
+ banner_text = Text()
29
+ banner_text.append("[*] DB-AGENT ", style="bold cyan")
30
+ banner_text.append("- Universal Database Introspector & AI Script Generator\n", style="bold white")
31
+ banner_text.append("Supports PostgreSQL | MySQL | SQLite | MSSQL | Oracle | DuckDB | MongoDB", style="dim")
32
+ panel = Panel(banner_text, border_style="cyan", padding=(0, 2))
33
+ console.print(panel)
34
+
35
+
36
+ def print_success(message: str) -> None:
37
+ console.print(f"[bold green][OK][/bold green] {message}")
38
+
39
+
40
+ def print_error(message: str) -> None:
41
+ console.print(f"[bold red][ERROR][/bold red] {message}")
42
+
43
+
44
+ def print_warning(message: str) -> None:
45
+ console.print(f"[bold yellow][WARN][/bold yellow] {message}")
46
+
47
+
48
+ def print_info(message: str) -> None:
49
+ console.print(f"[bold blue][INFO][/bold blue] {message}")
50
+
51
+
52
+ def print_code(code: str, language: str = "sql", title: Optional[str] = None) -> None:
53
+ """Print syntax-highlighted code block."""
54
+ syntax = Syntax(code, language, theme="monokai", line_numbers=True)
55
+ panel = Panel(syntax, title=f"[bold cyan]{title or language.upper()}[/bold cyan]", border_style="dim")
56
+ console.print(panel)
57
+
58
+
59
+ def print_results_table(columns: List[str], rows: List[Dict[str, Any]], title: str = "Query Results") -> None:
60
+ """Print query results in a clean Rich table."""
61
+ if not columns:
62
+ console.print("[dim]Query returned no rows.[/dim]")
63
+ return
64
+
65
+ table = Table(title=title, show_header=True, header_style="bold magenta", border_style="dim")
66
+ for col in columns:
67
+ table.add_column(str(col))
68
+
69
+ for row in rows:
70
+ row_vals = []
71
+ for col in columns:
72
+ v = row.get(col)
73
+ row_vals.append(str(v) if v is not None else "[dim]NULL[/dim]")
74
+ table.add_row(*row_vals)
75
+
76
+ console.print(table)
77
+ console.print(f"[dim]Total rows: {len(rows)}[/dim]\n")
dbagent/ui/viewer.py ADDED
@@ -0,0 +1,93 @@
1
+ """
2
+ Rich terminal viewer for database schema introspection.
3
+ """
4
+
5
+ from rich.table import Table
6
+ from rich.tree import Tree
7
+ from rich.panel import Panel
8
+ from rich.console import Console
9
+ from typing import Optional
10
+ from dbagent.schema.models import DatabaseSchema, TableModel
11
+ from dbagent.ui.console import console
12
+
13
+
14
+ class SchemaViewer:
15
+ """Renders database schema visual representations in the terminal."""
16
+
17
+ @staticmethod
18
+ def display_schema_summary(schema: DatabaseSchema) -> None:
19
+ """Display an overview table of all discovered tables and views."""
20
+ table = Table(
21
+ title=f"[Catalog] Database: [bold cyan]{schema.database_name}[/bold cyan] ({schema.dialect_name})",
22
+ show_header=True,
23
+ header_style="bold cyan",
24
+ border_style="dim",
25
+ )
26
+ table.add_column("Type", justify="center", width=8)
27
+ table.add_column("Table Name", style="bold white")
28
+ table.add_column("Columns", justify="right")
29
+ table.add_column("Primary Key", style="yellow")
30
+ table.add_column("Foreign Keys", style="green")
31
+ table.add_column("Est. Rows", justify="right", style="cyan")
32
+
33
+ for t in schema.tables:
34
+ type_tag = "[cyan]View[/cyan]" if t.is_view else "[white]Table[/white]"
35
+ pk_str = ", ".join(t.primary_key) if t.primary_key else "[dim]None[/dim]"
36
+ fk_count = len(t.foreign_keys)
37
+ fk_str = f"{fk_count} relations" if fk_count > 0 else "[dim]None[/dim]"
38
+ rows_str = f"{t.row_count:,}" if t.row_count is not None else "[dim]N/A[/dim]"
39
+
40
+ table.add_row(
41
+ type_tag,
42
+ t.name,
43
+ str(len(t.columns)),
44
+ pk_str,
45
+ fk_str,
46
+ rows_str,
47
+ )
48
+
49
+ console.print(table)
50
+ console.print(
51
+ f"[dim]Discovered [bold]{schema.total_tables}[/bold] tables and [bold]{schema.total_views}[/bold] views.[/dim]\n"
52
+ )
53
+
54
+ @staticmethod
55
+ def display_table_detail(table: TableModel) -> None:
56
+ """Display detailed column attributes for a single table."""
57
+ tbl = Table(
58
+ title=f"Table Structure: [bold cyan]{table.name}[/bold cyan]",
59
+ show_header=True,
60
+ header_style="bold magenta",
61
+ border_style="dim",
62
+ )
63
+ tbl.add_column("Column", style="bold white")
64
+ tbl.add_column("Type", style="cyan")
65
+ tbl.add_column("PK", justify="center", style="yellow")
66
+ tbl.add_column("Nullable", justify="center")
67
+ tbl.add_column("Default", style="dim")
68
+ tbl.add_column("Comment", style="italic dim")
69
+
70
+ for col in table.columns:
71
+ pk_str = "[PK]" if col.is_primary_key else ""
72
+ null_str = "[green]YES[/green]" if col.is_nullable else "[red]NO[/red]"
73
+ def_str = col.default_value or ""
74
+ comm_str = col.comment or ""
75
+ tbl.add_row(col.name, col.data_type, pk_str, null_str, def_str, comm_str)
76
+
77
+ console.print(tbl)
78
+
79
+ if table.foreign_keys:
80
+ console.print("[bold green]Foreign Keys:[/bold green]")
81
+ for fk in table.foreign_keys:
82
+ c = ", ".join(fk.constrained_columns)
83
+ r = ", ".join(fk.referred_columns)
84
+ console.print(f" * ({c}) -> [bold]{fk.referred_table}[/bold]({r})")
85
+ console.print("")
86
+
87
+ if table.indexes:
88
+ console.print("[bold blue]Indexes:[/bold blue]")
89
+ for idx in table.indexes:
90
+ u = "[yellow]UNIQUE [/yellow]" if idx.is_unique else ""
91
+ c = ", ".join(idx.columns)
92
+ console.print(f" * {u}{idx.name} on ({c})")
93
+ console.print("")