cinchdb 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.
- cinchdb/__init__.py +7 -0
- cinchdb/__main__.py +6 -0
- cinchdb/api/__init__.py +5 -0
- cinchdb/api/app.py +76 -0
- cinchdb/api/auth.py +290 -0
- cinchdb/api/main.py +137 -0
- cinchdb/api/routers/__init__.py +25 -0
- cinchdb/api/routers/auth.py +135 -0
- cinchdb/api/routers/branches.py +368 -0
- cinchdb/api/routers/codegen.py +164 -0
- cinchdb/api/routers/columns.py +290 -0
- cinchdb/api/routers/data.py +479 -0
- cinchdb/api/routers/databases.py +177 -0
- cinchdb/api/routers/projects.py +133 -0
- cinchdb/api/routers/query.py +156 -0
- cinchdb/api/routers/tables.py +349 -0
- cinchdb/api/routers/tenants.py +216 -0
- cinchdb/api/routers/views.py +219 -0
- cinchdb/cli/__init__.py +0 -0
- cinchdb/cli/commands/__init__.py +1 -0
- cinchdb/cli/commands/branch.py +479 -0
- cinchdb/cli/commands/codegen.py +176 -0
- cinchdb/cli/commands/column.py +308 -0
- cinchdb/cli/commands/database.py +212 -0
- cinchdb/cli/commands/query.py +136 -0
- cinchdb/cli/commands/remote.py +144 -0
- cinchdb/cli/commands/table.py +289 -0
- cinchdb/cli/commands/tenant.py +173 -0
- cinchdb/cli/commands/view.py +189 -0
- cinchdb/cli/handlers/__init__.py +5 -0
- cinchdb/cli/handlers/codegen_handler.py +189 -0
- cinchdb/cli/main.py +137 -0
- cinchdb/cli/utils.py +182 -0
- cinchdb/config.py +177 -0
- cinchdb/core/__init__.py +5 -0
- cinchdb/core/connection.py +175 -0
- cinchdb/core/database.py +537 -0
- cinchdb/core/maintenance.py +73 -0
- cinchdb/core/path_utils.py +153 -0
- cinchdb/managers/__init__.py +26 -0
- cinchdb/managers/branch.py +167 -0
- cinchdb/managers/change_applier.py +414 -0
- cinchdb/managers/change_comparator.py +194 -0
- cinchdb/managers/change_tracker.py +182 -0
- cinchdb/managers/codegen.py +523 -0
- cinchdb/managers/column.py +579 -0
- cinchdb/managers/data.py +455 -0
- cinchdb/managers/merge_manager.py +429 -0
- cinchdb/managers/query.py +214 -0
- cinchdb/managers/table.py +383 -0
- cinchdb/managers/tenant.py +258 -0
- cinchdb/managers/view.py +252 -0
- cinchdb/models/__init__.py +27 -0
- cinchdb/models/base.py +44 -0
- cinchdb/models/branch.py +26 -0
- cinchdb/models/change.py +47 -0
- cinchdb/models/database.py +20 -0
- cinchdb/models/project.py +20 -0
- cinchdb/models/table.py +86 -0
- cinchdb/models/tenant.py +19 -0
- cinchdb/models/view.py +15 -0
- cinchdb/utils/__init__.py +15 -0
- cinchdb/utils/sql_validator.py +137 -0
- cinchdb-0.1.0.dist-info/METADATA +195 -0
- cinchdb-0.1.0.dist-info/RECORD +68 -0
- cinchdb-0.1.0.dist-info/WHEEL +4 -0
- cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
- cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,479 @@
|
|
1
|
+
"""Branch management commands for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from pathlib import Path
|
6
|
+
from rich.console import Console
|
7
|
+
from rich.table import Table as RichTable
|
8
|
+
|
9
|
+
from cinchdb.config import Config
|
10
|
+
from cinchdb.core.path_utils import get_project_root
|
11
|
+
from cinchdb.managers.branch import BranchManager
|
12
|
+
from cinchdb.cli.utils import (
|
13
|
+
get_config_with_data,
|
14
|
+
set_active_branch,
|
15
|
+
validate_required_arg,
|
16
|
+
)
|
17
|
+
|
18
|
+
app = typer.Typer(help="Branch management commands", invoke_without_command=True)
|
19
|
+
console = Console()
|
20
|
+
|
21
|
+
|
22
|
+
@app.callback()
|
23
|
+
def callback(ctx: typer.Context):
|
24
|
+
"""Show help when no subcommand is provided."""
|
25
|
+
if ctx.invoked_subcommand is None:
|
26
|
+
console.print(ctx.get_help())
|
27
|
+
raise typer.Exit(0)
|
28
|
+
|
29
|
+
|
30
|
+
def get_config() -> Config:
|
31
|
+
"""Get config from current directory."""
|
32
|
+
project_root = get_project_root(Path.cwd())
|
33
|
+
if not project_root:
|
34
|
+
console.print("[red]❌ Not in a CinchDB project directory[/red]")
|
35
|
+
raise typer.Exit(1)
|
36
|
+
return Config(project_root)
|
37
|
+
|
38
|
+
|
39
|
+
@app.command(name="list")
|
40
|
+
def list_branches():
|
41
|
+
"""List all branches in the current database."""
|
42
|
+
config, config_data = get_config_with_data()
|
43
|
+
db_name = config_data.active_database
|
44
|
+
|
45
|
+
branch_mgr = BranchManager(config.project_dir, db_name)
|
46
|
+
branches = branch_mgr.list_branches()
|
47
|
+
|
48
|
+
if not branches:
|
49
|
+
console.print("[yellow]No branches found[/yellow]")
|
50
|
+
return
|
51
|
+
|
52
|
+
# Create a table
|
53
|
+
table = RichTable(title=f"Branches in '{db_name}'")
|
54
|
+
table.add_column("Name", style="cyan")
|
55
|
+
table.add_column("Active", style="green")
|
56
|
+
table.add_column("Parent", style="yellow")
|
57
|
+
table.add_column("Protected", style="red")
|
58
|
+
|
59
|
+
current_branch = config_data.active_branch
|
60
|
+
|
61
|
+
for branch in branches:
|
62
|
+
is_active = "✓" if branch.name == current_branch else ""
|
63
|
+
is_protected = "✓" if branch.name == "main" else ""
|
64
|
+
parent = branch.parent_branch or "-"
|
65
|
+
table.add_row(branch.name, is_active, parent, is_protected)
|
66
|
+
|
67
|
+
console.print(table)
|
68
|
+
|
69
|
+
|
70
|
+
@app.command()
|
71
|
+
def create(
|
72
|
+
ctx: typer.Context,
|
73
|
+
name: Optional[str] = typer.Argument(None, help="Name of the new branch"),
|
74
|
+
source: Optional[str] = typer.Option(
|
75
|
+
None, "--source", "-s", help="Source branch (default: current)"
|
76
|
+
),
|
77
|
+
switch: bool = typer.Option(
|
78
|
+
False, "--switch", help="Switch to the new branch after creation"
|
79
|
+
),
|
80
|
+
):
|
81
|
+
"""Create a new branch."""
|
82
|
+
name = validate_required_arg(name, "name", ctx)
|
83
|
+
config, config_data = get_config_with_data()
|
84
|
+
db_name = config_data.active_database
|
85
|
+
source_branch = source or config_data.active_branch
|
86
|
+
|
87
|
+
try:
|
88
|
+
branch_mgr = BranchManager(config.project_dir, db_name)
|
89
|
+
branch_mgr.create_branch(source_branch, name)
|
90
|
+
console.print(
|
91
|
+
f"[green]✅ Created branch '{name}' from '{source_branch}'[/green]"
|
92
|
+
)
|
93
|
+
|
94
|
+
if switch:
|
95
|
+
set_active_branch(config, name)
|
96
|
+
console.print(f"[green]✅ Switched to branch '{name}'[/green]")
|
97
|
+
|
98
|
+
except ValueError as e:
|
99
|
+
console.print(f"[red]❌ {e}[/red]")
|
100
|
+
raise typer.Exit(1)
|
101
|
+
|
102
|
+
|
103
|
+
@app.command()
|
104
|
+
def delete(
|
105
|
+
ctx: typer.Context,
|
106
|
+
name: Optional[str] = typer.Argument(None, help="Name of the branch to delete"),
|
107
|
+
force: bool = typer.Option(
|
108
|
+
False, "--force", "-f", help="Force deletion without confirmation"
|
109
|
+
),
|
110
|
+
):
|
111
|
+
"""Delete a branch."""
|
112
|
+
name = validate_required_arg(name, "name", ctx)
|
113
|
+
config, config_data = get_config_with_data()
|
114
|
+
db_name = config_data.active_database
|
115
|
+
|
116
|
+
if name == "main":
|
117
|
+
console.print("[red]❌ Cannot delete the main branch[/red]")
|
118
|
+
raise typer.Exit(1)
|
119
|
+
|
120
|
+
# Confirmation
|
121
|
+
if not force:
|
122
|
+
confirm = typer.confirm(f"Are you sure you want to delete branch '{name}'?")
|
123
|
+
if not confirm:
|
124
|
+
console.print("[yellow]Cancelled[/yellow]")
|
125
|
+
raise typer.Exit(0)
|
126
|
+
|
127
|
+
try:
|
128
|
+
branch_mgr = BranchManager(config.project_dir, db_name)
|
129
|
+
branch_mgr.delete_branch(name)
|
130
|
+
console.print(f"[green]✅ Deleted branch '{name}'[/green]")
|
131
|
+
|
132
|
+
except ValueError as e:
|
133
|
+
console.print(f"[red]❌ {e}[/red]")
|
134
|
+
raise typer.Exit(1)
|
135
|
+
|
136
|
+
|
137
|
+
@app.command()
|
138
|
+
def switch(
|
139
|
+
ctx: typer.Context,
|
140
|
+
name: Optional[str] = typer.Argument(None, help="Name of the branch to switch to"),
|
141
|
+
):
|
142
|
+
"""Switch to a different branch."""
|
143
|
+
name = validate_required_arg(name, "name", ctx)
|
144
|
+
config, config_data = get_config_with_data()
|
145
|
+
db_name = config_data.active_database
|
146
|
+
|
147
|
+
try:
|
148
|
+
BranchManager(config.project_dir, db_name)
|
149
|
+
set_active_branch(config, name)
|
150
|
+
console.print(f"[green]✅ Switched to branch '{name}'[/green]")
|
151
|
+
|
152
|
+
except ValueError as e:
|
153
|
+
console.print(f"[red]❌ {e}[/red]")
|
154
|
+
raise typer.Exit(1)
|
155
|
+
|
156
|
+
|
157
|
+
@app.command()
|
158
|
+
def info(
|
159
|
+
name: Optional[str] = typer.Argument(None, help="Branch name (default: current)"),
|
160
|
+
):
|
161
|
+
"""Show information about a branch."""
|
162
|
+
config, config_data = get_config_with_data()
|
163
|
+
db_name = config_data.active_database
|
164
|
+
branch_name = name or config_data.active_branch
|
165
|
+
|
166
|
+
try:
|
167
|
+
branch_mgr = BranchManager(config.project_dir, db_name)
|
168
|
+
branches = branch_mgr.list_branches()
|
169
|
+
|
170
|
+
branch = next((b for b in branches if b.name == branch_name), None)
|
171
|
+
if not branch:
|
172
|
+
console.print(f"[red]❌ Branch '{branch_name}' does not exist[/red]")
|
173
|
+
raise typer.Exit(1)
|
174
|
+
|
175
|
+
# Display info
|
176
|
+
console.print(f"\n[bold]Branch: {branch.name}[/bold]")
|
177
|
+
console.print(f"Database: {db_name}")
|
178
|
+
console.print(f"Parent: {branch.parent_branch or 'None'}")
|
179
|
+
created_at = branch.metadata.get("created_at", "Unknown")
|
180
|
+
if created_at != "Unknown":
|
181
|
+
from datetime import datetime
|
182
|
+
|
183
|
+
try:
|
184
|
+
dt = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
185
|
+
created_at = dt.strftime("%Y-%m-%d %H:%M:%S")
|
186
|
+
except Exception:
|
187
|
+
pass
|
188
|
+
console.print(f"Created: {created_at}")
|
189
|
+
console.print(f"Protected: {'Yes' if branch.name == 'main' else 'No'}")
|
190
|
+
|
191
|
+
# Count tenants
|
192
|
+
from cinchdb.managers.tenant import TenantManager
|
193
|
+
|
194
|
+
tenant_mgr = TenantManager(config.project_dir, db_name, branch_name)
|
195
|
+
tenants = tenant_mgr.list_tenants()
|
196
|
+
console.print(f"Tenants: {len(tenants)}")
|
197
|
+
|
198
|
+
# Count changes
|
199
|
+
from cinchdb.managers.change_tracker import ChangeTracker
|
200
|
+
|
201
|
+
tracker = ChangeTracker(config.project_dir, db_name, branch_name)
|
202
|
+
changes = tracker.get_changes()
|
203
|
+
unapplied = tracker.get_unapplied_changes()
|
204
|
+
console.print(f"Total Changes: {len(changes)}")
|
205
|
+
console.print(f"Unapplied Changes: {len(unapplied)}")
|
206
|
+
|
207
|
+
except ValueError as e:
|
208
|
+
console.print(f"[red]❌ {e}[/red]")
|
209
|
+
raise typer.Exit(1)
|
210
|
+
|
211
|
+
|
212
|
+
@app.command()
|
213
|
+
def merge(
|
214
|
+
ctx: typer.Context,
|
215
|
+
source: Optional[str] = typer.Argument(None, help="Source branch to merge from"),
|
216
|
+
target: Optional[str] = typer.Option(
|
217
|
+
None, "--target", "-t", help="Target branch (default: current)"
|
218
|
+
),
|
219
|
+
force: bool = typer.Option(
|
220
|
+
False, "--force", "-f", help="Force merge even with conflicts"
|
221
|
+
),
|
222
|
+
preview: bool = typer.Option(
|
223
|
+
False, "--preview", "-p", help="Show merge preview without executing"
|
224
|
+
),
|
225
|
+
dry_run: bool = typer.Option(
|
226
|
+
False,
|
227
|
+
"--dry-run",
|
228
|
+
help="Show SQL statements that would be executed without applying them",
|
229
|
+
),
|
230
|
+
):
|
231
|
+
"""Merge changes from source branch into target branch."""
|
232
|
+
source = validate_required_arg(source, "source", ctx)
|
233
|
+
config, config_data = get_config_with_data()
|
234
|
+
db_name = config_data.active_database
|
235
|
+
target_branch = target or config_data.active_branch
|
236
|
+
|
237
|
+
from cinchdb.managers.merge_manager import MergeManager, MergeError
|
238
|
+
|
239
|
+
try:
|
240
|
+
merge_mgr = MergeManager(config.project_dir, db_name)
|
241
|
+
|
242
|
+
if preview:
|
243
|
+
# Show merge preview
|
244
|
+
preview_result = merge_mgr.get_merge_preview(source, target_branch)
|
245
|
+
|
246
|
+
if not preview_result["can_merge"]:
|
247
|
+
console.print(f"[red]❌ Cannot merge: {preview_result['reason']}[/red]")
|
248
|
+
if "conflicts" in preview_result:
|
249
|
+
console.print("[yellow]Conflicts:[/yellow]")
|
250
|
+
for conflict in preview_result["conflicts"]:
|
251
|
+
console.print(f" • {conflict}")
|
252
|
+
raise typer.Exit(1)
|
253
|
+
|
254
|
+
console.print(f"\n[bold]Merge Preview: {source} → {target_branch}[/bold]")
|
255
|
+
console.print(f"Merge Type: {preview_result['merge_type']}")
|
256
|
+
console.print(f"Changes to merge: {preview_result['changes_to_merge']}")
|
257
|
+
console.print(
|
258
|
+
f"Target has changes: {preview_result.get('target_has_changes', False)}"
|
259
|
+
)
|
260
|
+
|
261
|
+
if preview_result["changes_by_type"]:
|
262
|
+
console.print("\n[bold]Changes by type:[/bold]")
|
263
|
+
for entity_type, changes in preview_result["changes_by_type"].items():
|
264
|
+
console.print(f" {entity_type}: {len(changes)} changes")
|
265
|
+
for change in changes[:3]: # Show first 3
|
266
|
+
console.print(
|
267
|
+
f" • {change['operation']} {change['entity_name']}"
|
268
|
+
)
|
269
|
+
if len(changes) > 3:
|
270
|
+
console.print(f" • ... and {len(changes) - 3} more")
|
271
|
+
|
272
|
+
return
|
273
|
+
|
274
|
+
# Handle dry-run
|
275
|
+
if dry_run:
|
276
|
+
result = merge_mgr.merge_branches(
|
277
|
+
source, target_branch, force=force, dry_run=True
|
278
|
+
)
|
279
|
+
|
280
|
+
console.print(f"\n[bold]Dry Run: {source} → {target_branch}[/bold]")
|
281
|
+
console.print(f"Merge Type: {result.get('merge_type', 'unknown')}")
|
282
|
+
console.print(f"Changes to merge: {result.get('changes_to_merge', 0)}")
|
283
|
+
|
284
|
+
if result.get("sql_statements"):
|
285
|
+
console.print("\n[bold]SQL statements that would be executed:[/bold]")
|
286
|
+
for stmt in result["sql_statements"]:
|
287
|
+
console.print(
|
288
|
+
f"\n[cyan]Change {stmt['change_id']} ({stmt['change_type']}): {stmt['entity_name']}[/cyan]"
|
289
|
+
)
|
290
|
+
if "step" in stmt:
|
291
|
+
console.print(f" Step: {stmt['step']}")
|
292
|
+
console.print(f" SQL: [yellow]{stmt['sql']}[/yellow]")
|
293
|
+
else:
|
294
|
+
console.print("\n[yellow]No SQL statements to execute[/yellow]")
|
295
|
+
|
296
|
+
return
|
297
|
+
|
298
|
+
# Perform actual merge
|
299
|
+
result = merge_mgr.merge_branches(source, target_branch, force=force)
|
300
|
+
|
301
|
+
if result["success"]:
|
302
|
+
console.print(f"[green]✅ {result['message']}[/green]")
|
303
|
+
console.print(f"Merge type: {result.get('merge_type', 'unknown')}")
|
304
|
+
else:
|
305
|
+
console.print(
|
306
|
+
f"[red]❌ Merge failed: {result.get('message', 'Unknown error')}[/red]"
|
307
|
+
)
|
308
|
+
raise typer.Exit(1)
|
309
|
+
|
310
|
+
except MergeError as e:
|
311
|
+
console.print(f"[red]❌ {e}[/red]")
|
312
|
+
raise typer.Exit(1)
|
313
|
+
except ValueError as e:
|
314
|
+
console.print(f"[red]❌ {e}[/red]")
|
315
|
+
raise typer.Exit(1)
|
316
|
+
|
317
|
+
|
318
|
+
@app.command()
|
319
|
+
def changes(
|
320
|
+
name: Optional[str] = typer.Argument(None, help="Branch name (default: current)"),
|
321
|
+
format: str = typer.Option(
|
322
|
+
"table", "--format", "-f", help="Output format (table, json)"
|
323
|
+
),
|
324
|
+
):
|
325
|
+
"""List all changes in a branch."""
|
326
|
+
config, config_data = get_config_with_data()
|
327
|
+
db_name = config_data.active_database
|
328
|
+
branch_name = name or config_data.active_branch
|
329
|
+
|
330
|
+
try:
|
331
|
+
from cinchdb.managers.change_tracker import ChangeTracker
|
332
|
+
|
333
|
+
tracker = ChangeTracker(config.project_dir, db_name, branch_name)
|
334
|
+
changes = tracker.get_changes()
|
335
|
+
|
336
|
+
if not changes:
|
337
|
+
console.print(f"[yellow]No changes found in branch '{branch_name}'[/yellow]")
|
338
|
+
return
|
339
|
+
|
340
|
+
if format == "json":
|
341
|
+
# JSON output
|
342
|
+
import json
|
343
|
+
from datetime import datetime
|
344
|
+
|
345
|
+
changes_data = []
|
346
|
+
for change in changes:
|
347
|
+
change_dict = change.model_dump(mode='json')
|
348
|
+
changes_data.append(change_dict)
|
349
|
+
|
350
|
+
console.print(json.dumps(changes_data, indent=2, default=str))
|
351
|
+
else:
|
352
|
+
# Table output
|
353
|
+
table = RichTable(title=f"Changes in '{branch_name}' branch")
|
354
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
355
|
+
table.add_column("Type", style="yellow")
|
356
|
+
table.add_column("Entity", style="green")
|
357
|
+
table.add_column("Entity Type", style="blue")
|
358
|
+
table.add_column("Applied", style="magenta")
|
359
|
+
table.add_column("Created", style="dim")
|
360
|
+
|
361
|
+
for change in changes:
|
362
|
+
created_at = change.created_at.strftime("%Y-%m-%d %H:%M:%S") if change.created_at else "Unknown"
|
363
|
+
applied_status = "✓" if change.applied else "✗"
|
364
|
+
table.add_row(
|
365
|
+
change.id[:8] if change.id else "Unknown",
|
366
|
+
change.type.value if hasattr(change.type, 'value') else str(change.type),
|
367
|
+
change.entity_name,
|
368
|
+
change.entity_type,
|
369
|
+
applied_status,
|
370
|
+
created_at
|
371
|
+
)
|
372
|
+
|
373
|
+
console.print(table)
|
374
|
+
|
375
|
+
# Summary
|
376
|
+
total = len(changes)
|
377
|
+
applied = sum(1 for c in changes if c.applied)
|
378
|
+
unapplied = total - applied
|
379
|
+
console.print(f"\n[bold]Total:[/bold] {total} changes ({applied} applied, {unapplied} unapplied)")
|
380
|
+
|
381
|
+
except ValueError as e:
|
382
|
+
console.print(f"[red]❌ {e}[/red]")
|
383
|
+
raise typer.Exit(1)
|
384
|
+
|
385
|
+
|
386
|
+
@app.command()
|
387
|
+
def merge_into_main(
|
388
|
+
ctx: typer.Context,
|
389
|
+
source: Optional[str] = typer.Argument(
|
390
|
+
None, help="Source branch to merge into main"
|
391
|
+
),
|
392
|
+
preview: bool = typer.Option(
|
393
|
+
False, "--preview", "-p", help="Show merge preview without executing"
|
394
|
+
),
|
395
|
+
dry_run: bool = typer.Option(
|
396
|
+
False,
|
397
|
+
"--dry-run",
|
398
|
+
help="Show SQL statements that would be executed without applying them",
|
399
|
+
),
|
400
|
+
):
|
401
|
+
"""Merge a branch into main branch (the primary way to get changes into main)."""
|
402
|
+
source = validate_required_arg(source, "source", ctx)
|
403
|
+
config, config_data = get_config_with_data()
|
404
|
+
db_name = config_data.active_database
|
405
|
+
|
406
|
+
from cinchdb.managers.merge_manager import MergeManager, MergeError
|
407
|
+
|
408
|
+
try:
|
409
|
+
merge_mgr = MergeManager(config.project_dir, db_name)
|
410
|
+
|
411
|
+
if preview:
|
412
|
+
# Show merge preview for main branch
|
413
|
+
preview_result = merge_mgr.get_merge_preview(source, "main")
|
414
|
+
|
415
|
+
if not preview_result["can_merge"]:
|
416
|
+
console.print(
|
417
|
+
f"[red]❌ Cannot merge into main: {preview_result['reason']}[/red]"
|
418
|
+
)
|
419
|
+
if "conflicts" in preview_result:
|
420
|
+
console.print("[yellow]Conflicts:[/yellow]")
|
421
|
+
for conflict in preview_result["conflicts"]:
|
422
|
+
console.print(f" • {conflict}")
|
423
|
+
raise typer.Exit(1)
|
424
|
+
|
425
|
+
console.print(f"\n[bold]Merge Preview: {source} → main[/bold]")
|
426
|
+
console.print(f"Merge Type: {preview_result['merge_type']}")
|
427
|
+
console.print(f"Changes to merge: {preview_result['changes_to_merge']}")
|
428
|
+
|
429
|
+
if preview_result["changes_by_type"]:
|
430
|
+
console.print("\n[bold]Changes to be merged:[/bold]")
|
431
|
+
for entity_type, changes in preview_result["changes_by_type"].items():
|
432
|
+
console.print(f" {entity_type}: {len(changes)} changes")
|
433
|
+
for change in changes:
|
434
|
+
console.print(
|
435
|
+
f" • {change['operation']} {change['entity_name']}"
|
436
|
+
)
|
437
|
+
|
438
|
+
return
|
439
|
+
|
440
|
+
# Handle dry-run
|
441
|
+
if dry_run:
|
442
|
+
result = merge_mgr.merge_into_main(source, dry_run=True)
|
443
|
+
|
444
|
+
console.print(f"\n[bold]Dry Run: {source} → main[/bold]")
|
445
|
+
console.print(f"Merge Type: {result.get('merge_type', 'unknown')}")
|
446
|
+
console.print(f"Changes to merge: {result.get('changes_to_merge', 0)}")
|
447
|
+
|
448
|
+
if result.get("sql_statements"):
|
449
|
+
console.print("\n[bold]SQL statements that would be executed:[/bold]")
|
450
|
+
for stmt in result["sql_statements"]:
|
451
|
+
console.print(
|
452
|
+
f"\n[cyan]Change {stmt['change_id']} ({stmt['change_type']}): {stmt['entity_name']}[/cyan]"
|
453
|
+
)
|
454
|
+
if "step" in stmt:
|
455
|
+
console.print(f" Step: {stmt['step']}")
|
456
|
+
console.print(f" SQL: [yellow]{stmt['sql']}[/yellow]")
|
457
|
+
else:
|
458
|
+
console.print("\n[yellow]No SQL statements to execute[/yellow]")
|
459
|
+
|
460
|
+
return
|
461
|
+
|
462
|
+
# Perform merge into main
|
463
|
+
result = merge_mgr.merge_into_main(source)
|
464
|
+
|
465
|
+
if result["success"]:
|
466
|
+
console.print(f"[green]✅ {result['message']}[/green]")
|
467
|
+
console.print("[green]Main branch has been updated![/green]")
|
468
|
+
else:
|
469
|
+
console.print(
|
470
|
+
f"[red]❌ Merge into main failed: {result.get('message', 'Unknown error')}[/red]"
|
471
|
+
)
|
472
|
+
raise typer.Exit(1)
|
473
|
+
|
474
|
+
except MergeError as e:
|
475
|
+
console.print(f"[red]❌ {e}[/red]")
|
476
|
+
raise typer.Exit(1)
|
477
|
+
except ValueError as e:
|
478
|
+
console.print(f"[red]❌ {e}[/red]")
|
479
|
+
raise typer.Exit(1)
|
@@ -0,0 +1,176 @@
|
|
1
|
+
"""Codegen CLI commands."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from pathlib import Path
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table as RichTable
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
from ..utils import get_config_with_data, get_config_dict, validate_required_arg
|
10
|
+
from ..handlers import CodegenHandler
|
11
|
+
|
12
|
+
console = Console()
|
13
|
+
|
14
|
+
app = typer.Typer(
|
15
|
+
help="Generate models from database schemas", invoke_without_command=True
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
@app.callback(invoke_without_command=True)
|
20
|
+
def main(ctx: typer.Context):
|
21
|
+
"""Generate models from database schemas."""
|
22
|
+
if ctx.invoked_subcommand is None:
|
23
|
+
print(ctx.get_help())
|
24
|
+
raise typer.Exit(0)
|
25
|
+
|
26
|
+
|
27
|
+
@app.command()
|
28
|
+
def languages():
|
29
|
+
"""List available code generation languages."""
|
30
|
+
try:
|
31
|
+
config, config_data = get_config_with_data()
|
32
|
+
config_dict = get_config_dict()
|
33
|
+
|
34
|
+
# Create handler to get supported languages
|
35
|
+
handler = CodegenHandler(config_dict)
|
36
|
+
supported = handler.get_supported_languages(project_root=config.project_dir)
|
37
|
+
|
38
|
+
table = RichTable(title="Supported Languages")
|
39
|
+
table.add_column("Language", style="cyan")
|
40
|
+
table.add_column("Status", style="green")
|
41
|
+
table.add_column("Source", style="blue")
|
42
|
+
|
43
|
+
for lang in supported:
|
44
|
+
status = "Available" if lang == "python" else "Coming Soon"
|
45
|
+
source = "Remote API" if handler.is_remote else "Local"
|
46
|
+
table.add_row(lang, status, source)
|
47
|
+
|
48
|
+
console.print(table)
|
49
|
+
|
50
|
+
except Exception as e:
|
51
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
52
|
+
raise typer.Exit(1)
|
53
|
+
|
54
|
+
|
55
|
+
@app.command()
|
56
|
+
def generate(
|
57
|
+
ctx: typer.Context,
|
58
|
+
language: Optional[str] = typer.Argument(
|
59
|
+
None, help="Target language (python, typescript)"
|
60
|
+
),
|
61
|
+
output_dir: Optional[str] = typer.Argument(
|
62
|
+
None, help="Output directory for generated models"
|
63
|
+
),
|
64
|
+
database: Optional[str] = typer.Option(
|
65
|
+
None, "--database", "-d", help="Database name (defaults to active)"
|
66
|
+
),
|
67
|
+
branch: Optional[str] = typer.Option(
|
68
|
+
None, "--branch", "-b", help="Branch name (defaults to active)"
|
69
|
+
),
|
70
|
+
tenant: str = typer.Option("main", "--tenant", "-t", help="Tenant name"),
|
71
|
+
include_tables: bool = typer.Option(
|
72
|
+
True, "--tables/--no-tables", help="Include table models"
|
73
|
+
),
|
74
|
+
include_views: bool = typer.Option(
|
75
|
+
True, "--views/--no-views", help="Include view models"
|
76
|
+
),
|
77
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing files"),
|
78
|
+
api_url: Optional[str] = typer.Option(
|
79
|
+
None, "--api-url", help="API URL for remote generation"
|
80
|
+
),
|
81
|
+
api_key: Optional[str] = typer.Option(
|
82
|
+
None, "--api-key", help="API key for remote generation"
|
83
|
+
),
|
84
|
+
local: bool = typer.Option(
|
85
|
+
False, "--local", help="Force local generation even if API configured"
|
86
|
+
),
|
87
|
+
):
|
88
|
+
"""Generate model files for the specified language."""
|
89
|
+
language = validate_required_arg(language, "language", ctx)
|
90
|
+
output_dir = validate_required_arg(output_dir, "output_dir", ctx)
|
91
|
+
try:
|
92
|
+
config, config_data = get_config_with_data()
|
93
|
+
config_dict = get_config_dict()
|
94
|
+
|
95
|
+
# Use provided values or defaults from config
|
96
|
+
db_name = database or config_data.active_database
|
97
|
+
branch_name = branch or config_data.active_branch
|
98
|
+
|
99
|
+
# Create output directory path
|
100
|
+
output_path = Path(output_dir).resolve()
|
101
|
+
|
102
|
+
# Check if output directory exists and has files
|
103
|
+
if output_path.exists() and any(output_path.iterdir()) and not force:
|
104
|
+
console.print(
|
105
|
+
f"[yellow]⚠️ Output directory '{output_path}' already contains files.[/yellow]"
|
106
|
+
)
|
107
|
+
console.print("Use --force to overwrite existing files.")
|
108
|
+
raise typer.Exit(1)
|
109
|
+
|
110
|
+
# Create handler with API options
|
111
|
+
handler = CodegenHandler(
|
112
|
+
config_data=config_dict, api_url=api_url, api_key=api_key, force_local=local
|
113
|
+
)
|
114
|
+
|
115
|
+
# Validate language
|
116
|
+
supported = handler.get_supported_languages(project_root=config.project_dir)
|
117
|
+
if language not in supported:
|
118
|
+
console.print(f"[red]❌ Language '{language}' not supported.[/red]")
|
119
|
+
console.print(f"Available languages: {', '.join(supported)}")
|
120
|
+
raise typer.Exit(1)
|
121
|
+
|
122
|
+
# Show what will be generated
|
123
|
+
source = "Remote API" if handler.is_remote else "Local"
|
124
|
+
console.print(f"[blue]🔧 Generating {language} models via {source}...[/blue]")
|
125
|
+
console.print(f"Database: {db_name}")
|
126
|
+
console.print(f"Branch: {branch_name}")
|
127
|
+
console.print(f"Tenant: {tenant}")
|
128
|
+
console.print(f"Output: {output_path}")
|
129
|
+
console.print(f"Include tables: {include_tables}")
|
130
|
+
console.print(f"Include views: {include_views}")
|
131
|
+
if handler.is_remote:
|
132
|
+
console.print(f"API URL: {handler.api_url}")
|
133
|
+
console.print()
|
134
|
+
|
135
|
+
# Generate models
|
136
|
+
results = handler.generate_models(
|
137
|
+
language=language,
|
138
|
+
output_dir=output_path,
|
139
|
+
database=db_name,
|
140
|
+
branch=branch_name,
|
141
|
+
tenant=tenant,
|
142
|
+
include_tables=include_tables,
|
143
|
+
include_views=include_views,
|
144
|
+
project_root=config.project_dir,
|
145
|
+
)
|
146
|
+
|
147
|
+
# Display results
|
148
|
+
console.print(
|
149
|
+
f"[green]✅ Generated {len(results['files_generated'])} files[/green]"
|
150
|
+
)
|
151
|
+
|
152
|
+
if results.get("tables_processed"):
|
153
|
+
console.print(f"Tables processed: {', '.join(results['tables_processed'])}")
|
154
|
+
|
155
|
+
if results.get("views_processed"):
|
156
|
+
console.print(f"Views processed: {', '.join(results['views_processed'])}")
|
157
|
+
|
158
|
+
console.print(f"Output directory: {results['output_dir']}")
|
159
|
+
|
160
|
+
# Show generated files
|
161
|
+
if results["files_generated"]:
|
162
|
+
table = RichTable(title="Generated Files")
|
163
|
+
table.add_column("File", style="cyan")
|
164
|
+
|
165
|
+
for filename in results["files_generated"]:
|
166
|
+
table.add_row(filename)
|
167
|
+
|
168
|
+
console.print(table)
|
169
|
+
|
170
|
+
except Exception as e:
|
171
|
+
console.print(f"[red]❌ Error: {e}[/red]")
|
172
|
+
raise typer.Exit(1)
|
173
|
+
|
174
|
+
|
175
|
+
if __name__ == "__main__":
|
176
|
+
app()
|