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,308 @@
|
|
1
|
+
"""Column management commands for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table as RichTable
|
7
|
+
|
8
|
+
from cinchdb.managers.column import ColumnManager
|
9
|
+
from cinchdb.managers.change_applier import ChangeApplier
|
10
|
+
from cinchdb.models import Column
|
11
|
+
from cinchdb.cli.utils import get_config_with_data, validate_required_arg
|
12
|
+
|
13
|
+
app = typer.Typer(help="Column management commands", invoke_without_command=True)
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
|
17
|
+
@app.callback()
|
18
|
+
def callback(ctx: typer.Context):
|
19
|
+
"""Show help when no subcommand is provided."""
|
20
|
+
if ctx.invoked_subcommand is None:
|
21
|
+
console.print(ctx.get_help())
|
22
|
+
raise typer.Exit(0)
|
23
|
+
|
24
|
+
|
25
|
+
@app.command(name="list")
|
26
|
+
def list_columns(
|
27
|
+
ctx: typer.Context, table: Optional[str] = typer.Argument(None, help="Table name")
|
28
|
+
):
|
29
|
+
"""List all columns in a table."""
|
30
|
+
table = validate_required_arg(table, "table", ctx)
|
31
|
+
config, config_data = get_config_with_data()
|
32
|
+
db_name = config_data.active_database
|
33
|
+
branch_name = config_data.active_branch
|
34
|
+
|
35
|
+
try:
|
36
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
37
|
+
columns = column_mgr.list_columns(table)
|
38
|
+
|
39
|
+
# Create a table
|
40
|
+
col_table = RichTable(title=f"Columns in '{table}'")
|
41
|
+
col_table.add_column("Name", style="cyan")
|
42
|
+
col_table.add_column("Type", style="green")
|
43
|
+
col_table.add_column("Nullable", style="yellow")
|
44
|
+
col_table.add_column("Primary Key", style="red")
|
45
|
+
col_table.add_column("Default", style="blue")
|
46
|
+
|
47
|
+
for col in columns:
|
48
|
+
nullable = "Yes" if col.nullable else "No"
|
49
|
+
pk = "Yes" if col.primary_key else "No"
|
50
|
+
default = col.default or "-"
|
51
|
+
col_table.add_row(col.name, col.type, nullable, pk, default)
|
52
|
+
|
53
|
+
console.print(col_table)
|
54
|
+
|
55
|
+
except ValueError as e:
|
56
|
+
console.print(f"[red]❌ {e}[/red]")
|
57
|
+
raise typer.Exit(1)
|
58
|
+
|
59
|
+
|
60
|
+
@app.command()
|
61
|
+
def add(
|
62
|
+
ctx: typer.Context,
|
63
|
+
table: Optional[str] = typer.Argument(None, help="Table name"),
|
64
|
+
name: Optional[str] = typer.Argument(None, help="Column name"),
|
65
|
+
type: Optional[str] = typer.Argument(
|
66
|
+
None, help="Column type (TEXT, INTEGER, REAL, BLOB, NUMERIC)"
|
67
|
+
),
|
68
|
+
nullable: bool = typer.Option(
|
69
|
+
True, "--nullable/--not-null", help="Allow NULL values"
|
70
|
+
),
|
71
|
+
default: Optional[str] = typer.Option(
|
72
|
+
None, "--default", "-d", help="Default value"
|
73
|
+
),
|
74
|
+
apply: bool = typer.Option(
|
75
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
76
|
+
),
|
77
|
+
):
|
78
|
+
"""Add a new column to a table."""
|
79
|
+
table = validate_required_arg(table, "table", ctx)
|
80
|
+
name = validate_required_arg(name, "name", ctx)
|
81
|
+
type = validate_required_arg(type, "type", ctx)
|
82
|
+
config, config_data = get_config_with_data()
|
83
|
+
db_name = config_data.active_database
|
84
|
+
branch_name = config_data.active_branch
|
85
|
+
|
86
|
+
# Validate type
|
87
|
+
type = type.upper()
|
88
|
+
if type not in ["TEXT", "INTEGER", "REAL", "BLOB", "NUMERIC"]:
|
89
|
+
console.print(f"[red]❌ Invalid type: '{type}'[/red]")
|
90
|
+
console.print(
|
91
|
+
"[yellow]Valid types: TEXT, INTEGER, REAL, BLOB, NUMERIC[/yellow]"
|
92
|
+
)
|
93
|
+
raise typer.Exit(1)
|
94
|
+
|
95
|
+
try:
|
96
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
97
|
+
column = Column(name=name, type=type, nullable=nullable, default=default)
|
98
|
+
column_mgr.add_column(table, column)
|
99
|
+
|
100
|
+
console.print(f"[green]✅ Added column '{name}' to table '{table}'[/green]")
|
101
|
+
|
102
|
+
if apply:
|
103
|
+
# Apply to all tenants
|
104
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
105
|
+
applied = applier.apply_all_unapplied()
|
106
|
+
if applied > 0:
|
107
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
108
|
+
|
109
|
+
except ValueError as e:
|
110
|
+
console.print(f"[red]❌ {e}[/red]")
|
111
|
+
raise typer.Exit(1)
|
112
|
+
|
113
|
+
|
114
|
+
@app.command()
|
115
|
+
def drop(
|
116
|
+
ctx: typer.Context,
|
117
|
+
table: Optional[str] = typer.Argument(None, help="Table name"),
|
118
|
+
name: Optional[str] = typer.Argument(None, help="Column name to drop"),
|
119
|
+
force: bool = typer.Option(
|
120
|
+
False, "--force", "-f", help="Force deletion without confirmation"
|
121
|
+
),
|
122
|
+
apply: bool = typer.Option(
|
123
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
124
|
+
),
|
125
|
+
):
|
126
|
+
"""Drop a column from a table."""
|
127
|
+
table = validate_required_arg(table, "table", ctx)
|
128
|
+
name = validate_required_arg(name, "name", ctx)
|
129
|
+
config, config_data = get_config_with_data()
|
130
|
+
db_name = config_data.active_database
|
131
|
+
branch_name = config_data.active_branch
|
132
|
+
|
133
|
+
# Confirmation
|
134
|
+
if not force:
|
135
|
+
confirm = typer.confirm(
|
136
|
+
f"Are you sure you want to drop column '{name}' from table '{table}'?"
|
137
|
+
)
|
138
|
+
if not confirm:
|
139
|
+
console.print("[yellow]Cancelled[/yellow]")
|
140
|
+
raise typer.Exit(0)
|
141
|
+
|
142
|
+
try:
|
143
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
144
|
+
column_mgr.drop_column(table, name)
|
145
|
+
|
146
|
+
console.print(f"[green]✅ Dropped column '{name}' from table '{table}'[/green]")
|
147
|
+
|
148
|
+
if apply:
|
149
|
+
# Apply to all tenants
|
150
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
151
|
+
applied = applier.apply_all_unapplied()
|
152
|
+
if applied > 0:
|
153
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
154
|
+
|
155
|
+
except ValueError as e:
|
156
|
+
console.print(f"[red]❌ {e}[/red]")
|
157
|
+
raise typer.Exit(1)
|
158
|
+
|
159
|
+
|
160
|
+
@app.command()
|
161
|
+
def rename(
|
162
|
+
ctx: typer.Context,
|
163
|
+
table: Optional[str] = typer.Argument(None, help="Table name"),
|
164
|
+
old_name: Optional[str] = typer.Argument(None, help="Current column name"),
|
165
|
+
new_name: Optional[str] = typer.Argument(None, help="New column name"),
|
166
|
+
apply: bool = typer.Option(
|
167
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
168
|
+
),
|
169
|
+
):
|
170
|
+
"""Rename a column in a table."""
|
171
|
+
table = validate_required_arg(table, "table", ctx)
|
172
|
+
old_name = validate_required_arg(old_name, "old_name", ctx)
|
173
|
+
new_name = validate_required_arg(new_name, "new_name", ctx)
|
174
|
+
config, config_data = get_config_with_data()
|
175
|
+
db_name = config_data.active_database
|
176
|
+
branch_name = config_data.active_branch
|
177
|
+
|
178
|
+
try:
|
179
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
180
|
+
column_mgr.rename_column(table, old_name, new_name)
|
181
|
+
|
182
|
+
console.print(
|
183
|
+
f"[green]✅ Renamed column '{old_name}' to '{new_name}' in table '{table}'[/green]"
|
184
|
+
)
|
185
|
+
|
186
|
+
if apply:
|
187
|
+
# Apply to all tenants
|
188
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
189
|
+
applied = applier.apply_all_unapplied()
|
190
|
+
if applied > 0:
|
191
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
192
|
+
|
193
|
+
except ValueError as e:
|
194
|
+
console.print(f"[red]❌ {e}[/red]")
|
195
|
+
raise typer.Exit(1)
|
196
|
+
|
197
|
+
|
198
|
+
@app.command()
|
199
|
+
def info(
|
200
|
+
ctx: typer.Context,
|
201
|
+
table: Optional[str] = typer.Argument(None, help="Table name"),
|
202
|
+
name: Optional[str] = typer.Argument(None, help="Column name"),
|
203
|
+
):
|
204
|
+
"""Show detailed information about a column."""
|
205
|
+
table = validate_required_arg(table, "table", ctx)
|
206
|
+
name = validate_required_arg(name, "name", ctx)
|
207
|
+
config, config_data = get_config_with_data()
|
208
|
+
db_name = config_data.active_database
|
209
|
+
branch_name = config_data.active_branch
|
210
|
+
|
211
|
+
try:
|
212
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
213
|
+
column = column_mgr.get_column_info(table, name)
|
214
|
+
|
215
|
+
# Display info
|
216
|
+
console.print(f"\n[bold]Column: {column.name}[/bold]")
|
217
|
+
console.print(f"Table: {table}")
|
218
|
+
console.print(f"Type: {column.type}")
|
219
|
+
console.print(f"Nullable: {'Yes' if column.nullable else 'No'}")
|
220
|
+
console.print(f"Primary Key: {'Yes' if column.primary_key else 'No'}")
|
221
|
+
console.print(f"Unique: {'Yes' if column.unique else 'No'}")
|
222
|
+
console.print(f"Default: {column.default or 'None'}")
|
223
|
+
|
224
|
+
except ValueError as e:
|
225
|
+
console.print(f"[red]❌ {e}[/red]")
|
226
|
+
raise typer.Exit(1)
|
227
|
+
|
228
|
+
|
229
|
+
@app.command(name="alter-nullable")
|
230
|
+
def alter_nullable(
|
231
|
+
ctx: typer.Context,
|
232
|
+
table: Optional[str] = typer.Argument(None, help="Table name"),
|
233
|
+
column: Optional[str] = typer.Argument(None, help="Column name"),
|
234
|
+
nullable: bool = typer.Option(
|
235
|
+
None, "--nullable/--not-nullable", help="Make column nullable or NOT NULL"
|
236
|
+
),
|
237
|
+
fill_value: Optional[str] = typer.Option(
|
238
|
+
None, "--fill-value", "-f", help="Value to use for NULL values when making NOT NULL"
|
239
|
+
),
|
240
|
+
apply: bool = typer.Option(
|
241
|
+
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
242
|
+
),
|
243
|
+
):
|
244
|
+
"""Change the nullable constraint on a column."""
|
245
|
+
table = validate_required_arg(table, "table", ctx)
|
246
|
+
column = validate_required_arg(column, "column", ctx)
|
247
|
+
|
248
|
+
# Validate nullable flag was provided
|
249
|
+
if nullable is None:
|
250
|
+
console.print(ctx.get_help())
|
251
|
+
console.print("\n[red]❌ Error: Must specify either --nullable or --not-nullable[/red]")
|
252
|
+
raise typer.Exit(1)
|
253
|
+
|
254
|
+
config, config_data = get_config_with_data()
|
255
|
+
db_name = config_data.active_database
|
256
|
+
branch_name = config_data.active_branch
|
257
|
+
|
258
|
+
try:
|
259
|
+
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
260
|
+
|
261
|
+
# Check if column has NULLs when making NOT NULL
|
262
|
+
if not nullable and fill_value is None:
|
263
|
+
# Get column info to check current state
|
264
|
+
col_info = column_mgr.get_column_info(table, column)
|
265
|
+
if col_info.nullable:
|
266
|
+
# Check for NULL values
|
267
|
+
from cinchdb.core.connection import DatabaseConnection
|
268
|
+
from cinchdb.core.path_utils import get_tenant_db_path
|
269
|
+
|
270
|
+
db_path = get_tenant_db_path(config.project_dir, db_name, branch_name, "main")
|
271
|
+
with DatabaseConnection(db_path) as conn:
|
272
|
+
cursor = conn.execute(
|
273
|
+
f"SELECT COUNT(*) FROM {table} WHERE {column} IS NULL"
|
274
|
+
)
|
275
|
+
null_count = cursor.fetchone()[0]
|
276
|
+
|
277
|
+
if null_count > 0:
|
278
|
+
console.print(f"[yellow]Column '{column}' has {null_count} NULL values.[/yellow]")
|
279
|
+
fill_value = typer.prompt("Provide a fill value")
|
280
|
+
|
281
|
+
# Convert fill_value to appropriate type
|
282
|
+
if fill_value is not None:
|
283
|
+
# Try to interpret the value
|
284
|
+
if fill_value.lower() == "null":
|
285
|
+
fill_value = None
|
286
|
+
elif fill_value.isdigit():
|
287
|
+
fill_value = int(fill_value)
|
288
|
+
elif fill_value.replace(".", "", 1).isdigit():
|
289
|
+
fill_value = float(fill_value)
|
290
|
+
# Otherwise keep as string
|
291
|
+
|
292
|
+
column_mgr.alter_column_nullable(table, column, nullable, fill_value)
|
293
|
+
|
294
|
+
if nullable:
|
295
|
+
console.print(f"[green]✅ Made column '{column}' nullable in table '{table}'[/green]")
|
296
|
+
else:
|
297
|
+
console.print(f"[green]✅ Made column '{column}' NOT NULL in table '{table}'[/green]")
|
298
|
+
|
299
|
+
if apply:
|
300
|
+
# Apply to all tenants
|
301
|
+
applier = ChangeApplier(config.project_dir, db_name, branch_name)
|
302
|
+
applied = applier.apply_all_unapplied()
|
303
|
+
if applied > 0:
|
304
|
+
console.print("[green]✅ Applied changes to all tenants[/green]")
|
305
|
+
|
306
|
+
except ValueError as e:
|
307
|
+
console.print(f"[red]❌ {e}[/red]")
|
308
|
+
raise typer.Exit(1)
|
@@ -0,0 +1,212 @@
|
|
1
|
+
"""Database management commands for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table as RichTable
|
7
|
+
|
8
|
+
from cinchdb.core.path_utils import list_databases
|
9
|
+
from cinchdb.cli.utils import (
|
10
|
+
get_config_with_data,
|
11
|
+
set_active_database,
|
12
|
+
set_active_branch,
|
13
|
+
validate_required_arg,
|
14
|
+
)
|
15
|
+
|
16
|
+
app = typer.Typer(help="Database management commands", invoke_without_command=True)
|
17
|
+
console = Console()
|
18
|
+
|
19
|
+
|
20
|
+
@app.callback(invoke_without_command=True)
|
21
|
+
def main(ctx: typer.Context):
|
22
|
+
"""Database management commands."""
|
23
|
+
if ctx.invoked_subcommand is None:
|
24
|
+
print(ctx.get_help())
|
25
|
+
raise typer.Exit(0)
|
26
|
+
|
27
|
+
|
28
|
+
@app.command(name="list")
|
29
|
+
def list_dbs():
|
30
|
+
"""List all databases in the project."""
|
31
|
+
config, config_data = get_config_with_data()
|
32
|
+
databases = list_databases(config.project_dir)
|
33
|
+
|
34
|
+
if not databases:
|
35
|
+
console.print("[yellow]No databases found[/yellow]")
|
36
|
+
return
|
37
|
+
|
38
|
+
# Create a table
|
39
|
+
table = RichTable(title="Databases")
|
40
|
+
table.add_column("Name", style="cyan")
|
41
|
+
table.add_column("Active", style="green")
|
42
|
+
table.add_column("Protected", style="yellow")
|
43
|
+
|
44
|
+
current_db = config_data.active_database
|
45
|
+
|
46
|
+
for db_name in databases:
|
47
|
+
is_active = "✓" if db_name == current_db else ""
|
48
|
+
is_protected = "✓" if db_name == "main" else ""
|
49
|
+
table.add_row(db_name, is_active, is_protected)
|
50
|
+
|
51
|
+
console.print(table)
|
52
|
+
|
53
|
+
|
54
|
+
@app.command()
|
55
|
+
def create(
|
56
|
+
ctx: typer.Context,
|
57
|
+
name: Optional[str] = typer.Argument(None, help="Name of the database to create"),
|
58
|
+
description: Optional[str] = typer.Option(
|
59
|
+
None, "--description", "-d", help="Database description"
|
60
|
+
),
|
61
|
+
switch: bool = typer.Option(
|
62
|
+
False, "--switch", "-s", help="Switch to the new database after creation"
|
63
|
+
),
|
64
|
+
):
|
65
|
+
"""Create a new database."""
|
66
|
+
name = validate_required_arg(name, "name", ctx)
|
67
|
+
config, config_data = get_config_with_data()
|
68
|
+
|
69
|
+
# Create database directory structure
|
70
|
+
db_path = config.project_dir / ".cinchdb" / "databases" / name
|
71
|
+
if db_path.exists():
|
72
|
+
console.print(f"[red]❌ Database '{name}' already exists[/red]")
|
73
|
+
raise typer.Exit(1)
|
74
|
+
|
75
|
+
# Create the database structure
|
76
|
+
db_path.mkdir(parents=True)
|
77
|
+
branches_dir = db_path / "branches"
|
78
|
+
branches_dir.mkdir()
|
79
|
+
|
80
|
+
# Create main branch
|
81
|
+
main_branch = branches_dir / "main"
|
82
|
+
main_branch.mkdir()
|
83
|
+
|
84
|
+
# Create main tenant
|
85
|
+
tenants_dir = main_branch / "tenants"
|
86
|
+
tenants_dir.mkdir()
|
87
|
+
main_tenant = tenants_dir / "main.db"
|
88
|
+
main_tenant.touch()
|
89
|
+
|
90
|
+
# Create branch metadata
|
91
|
+
import json
|
92
|
+
from datetime import datetime, timezone
|
93
|
+
|
94
|
+
metadata = {
|
95
|
+
"name": "main",
|
96
|
+
"parent": None,
|
97
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
98
|
+
}
|
99
|
+
with open(main_branch / "metadata.json", "w") as f:
|
100
|
+
json.dump(metadata, f, indent=2)
|
101
|
+
|
102
|
+
# Create empty changes file
|
103
|
+
with open(main_branch / "changes.json", "w") as f:
|
104
|
+
json.dump([], f)
|
105
|
+
|
106
|
+
console.print(f"[green]✅ Created database '{name}'[/green]")
|
107
|
+
|
108
|
+
if switch:
|
109
|
+
set_active_database(config, name)
|
110
|
+
console.print(f"[green]✅ Switched to database '{name}'[/green]")
|
111
|
+
|
112
|
+
|
113
|
+
@app.command()
|
114
|
+
def delete(
|
115
|
+
ctx: typer.Context,
|
116
|
+
name: Optional[str] = typer.Argument(None, help="Name of the database to delete"),
|
117
|
+
force: bool = typer.Option(
|
118
|
+
False, "--force", "-f", help="Force deletion without confirmation"
|
119
|
+
),
|
120
|
+
):
|
121
|
+
"""Delete a database."""
|
122
|
+
name = validate_required_arg(name, "name", ctx)
|
123
|
+
if name == "main":
|
124
|
+
console.print("[red]❌ Cannot delete the main database[/red]")
|
125
|
+
raise typer.Exit(1)
|
126
|
+
|
127
|
+
config, config_data = get_config_with_data()
|
128
|
+
db_path = config.project_dir / ".cinchdb" / "databases" / name
|
129
|
+
|
130
|
+
if not db_path.exists():
|
131
|
+
console.print(f"[red]❌ Database '{name}' does not exist[/red]")
|
132
|
+
raise typer.Exit(1)
|
133
|
+
|
134
|
+
# Confirmation
|
135
|
+
if not force:
|
136
|
+
confirm = typer.confirm(f"Are you sure you want to delete database '{name}'?")
|
137
|
+
if not confirm:
|
138
|
+
console.print("[yellow]Cancelled[/yellow]")
|
139
|
+
raise typer.Exit(0)
|
140
|
+
|
141
|
+
# Delete the database
|
142
|
+
import shutil
|
143
|
+
|
144
|
+
shutil.rmtree(db_path)
|
145
|
+
|
146
|
+
# If this was the active database, switch to main
|
147
|
+
if config_data.active_database == name:
|
148
|
+
set_active_database(config, "main")
|
149
|
+
console.print("[yellow]Switched to main database[/yellow]")
|
150
|
+
|
151
|
+
console.print(f"[green]✅ Deleted database '{name}'[/green]")
|
152
|
+
|
153
|
+
|
154
|
+
@app.command()
|
155
|
+
def info(
|
156
|
+
name: Optional[str] = typer.Argument(None, help="Database name (default: current)"),
|
157
|
+
):
|
158
|
+
"""Show information about a database."""
|
159
|
+
config, config_data = get_config_with_data()
|
160
|
+
db_name = name or config_data.active_database
|
161
|
+
|
162
|
+
db_path = config.project_dir / ".cinchdb" / "databases" / db_name
|
163
|
+
if not db_path.exists():
|
164
|
+
console.print(f"[red]❌ Database '{db_name}' does not exist[/red]")
|
165
|
+
raise typer.Exit(1)
|
166
|
+
|
167
|
+
# Count branches
|
168
|
+
branches_path = db_path / "branches"
|
169
|
+
branch_count = len(list(branches_path.iterdir())) if branches_path.exists() else 0
|
170
|
+
|
171
|
+
# Get active branch
|
172
|
+
active_branch = config_data.active_branch
|
173
|
+
|
174
|
+
# Display info
|
175
|
+
console.print(f"\n[bold]Database: {db_name}[/bold]")
|
176
|
+
console.print(f"Location: {db_path}")
|
177
|
+
console.print(f"Branches: {branch_count}")
|
178
|
+
console.print(f"Active Branch: {active_branch}")
|
179
|
+
console.print(f"Protected: {'Yes' if db_name == 'main' else 'No'}")
|
180
|
+
|
181
|
+
# List branches
|
182
|
+
if branch_count > 0:
|
183
|
+
console.print("\n[bold]Branches:[/bold]")
|
184
|
+
for branch_dir in sorted(branches_path.iterdir()):
|
185
|
+
if branch_dir.is_dir():
|
186
|
+
branch_name = branch_dir.name
|
187
|
+
is_active = " (active)" if branch_name == active_branch else ""
|
188
|
+
console.print(f" - {branch_name}{is_active}")
|
189
|
+
|
190
|
+
|
191
|
+
@app.command()
|
192
|
+
def switch(
|
193
|
+
ctx: typer.Context,
|
194
|
+
name: Optional[str] = typer.Argument(
|
195
|
+
None, help="Name of the database to switch to"
|
196
|
+
),
|
197
|
+
):
|
198
|
+
"""Switch to a different database."""
|
199
|
+
name = validate_required_arg(name, "name", ctx)
|
200
|
+
config, config_data = get_config_with_data()
|
201
|
+
|
202
|
+
# Check if database exists
|
203
|
+
db_path = config.project_dir / ".cinchdb" / "databases" / name
|
204
|
+
if not db_path.exists():
|
205
|
+
console.print(f"[red]❌ Database '{name}' does not exist[/red]")
|
206
|
+
raise typer.Exit(1)
|
207
|
+
|
208
|
+
# Switch
|
209
|
+
set_active_database(config, name)
|
210
|
+
set_active_branch(config, "main") # Always switch to main branch
|
211
|
+
|
212
|
+
console.print(f"[green]✅ Switched to database '{name}'[/green]")
|
@@ -0,0 +1,136 @@
|
|
1
|
+
"""Query execution command for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import Optional
|
5
|
+
from rich.console import Console
|
6
|
+
from rich.table import Table as RichTable
|
7
|
+
|
8
|
+
from cinchdb.cli.utils import get_config_with_data, get_cinchdb_instance
|
9
|
+
from cinchdb.managers.query import QueryManager
|
10
|
+
|
11
|
+
app = typer.Typer(help="Execute SQL queries", invoke_without_command=True)
|
12
|
+
console = Console()
|
13
|
+
|
14
|
+
|
15
|
+
def execute_query(sql: str, tenant: str, format: str, limit: Optional[int], force_local: bool = False, remote_alias: Optional[str] = None):
|
16
|
+
"""Execute a SQL query."""
|
17
|
+
# Add LIMIT if specified
|
18
|
+
query_sql = sql
|
19
|
+
if limit and "LIMIT" not in sql.upper():
|
20
|
+
query_sql = f"{sql} LIMIT {limit}"
|
21
|
+
|
22
|
+
# Get CinchDB instance (handles local/remote automatically)
|
23
|
+
db = get_cinchdb_instance(tenant=tenant, force_local=force_local, remote_alias=remote_alias)
|
24
|
+
|
25
|
+
try:
|
26
|
+
# Check if this is a SELECT query
|
27
|
+
is_select = query_sql.strip().upper().startswith("SELECT")
|
28
|
+
|
29
|
+
if is_select:
|
30
|
+
# Execute SELECT query
|
31
|
+
rows = db.query(query_sql)
|
32
|
+
|
33
|
+
if not rows:
|
34
|
+
console.print("[yellow]No results[/yellow]")
|
35
|
+
return
|
36
|
+
|
37
|
+
# Get column names from first row
|
38
|
+
columns = list(rows[0].keys()) if rows else []
|
39
|
+
|
40
|
+
if format == "json":
|
41
|
+
# JSON output - rows are already dicts
|
42
|
+
console.print_json(data=rows)
|
43
|
+
|
44
|
+
elif format == "csv":
|
45
|
+
# CSV output
|
46
|
+
import csv
|
47
|
+
import sys
|
48
|
+
|
49
|
+
writer = csv.writer(sys.stdout)
|
50
|
+
writer.writerow(columns)
|
51
|
+
for row in rows:
|
52
|
+
writer.writerow([row[col] for col in columns])
|
53
|
+
|
54
|
+
else:
|
55
|
+
# Table output (default)
|
56
|
+
table = RichTable(title=f"Query Results ({len(rows)} rows)")
|
57
|
+
|
58
|
+
# Add columns
|
59
|
+
for col in columns:
|
60
|
+
table.add_column(col, style="cyan")
|
61
|
+
|
62
|
+
# Add rows
|
63
|
+
for row in rows:
|
64
|
+
# Convert all values to strings
|
65
|
+
str_row = [
|
66
|
+
str(row[col]) if row[col] is not None else "NULL"
|
67
|
+
for col in columns
|
68
|
+
]
|
69
|
+
table.add_row(*str_row)
|
70
|
+
|
71
|
+
console.print(table)
|
72
|
+
else:
|
73
|
+
# For INSERT/UPDATE/DELETE, we need to handle local vs remote differently
|
74
|
+
if db.is_local:
|
75
|
+
# For local connections, use the query manager directly
|
76
|
+
from cinchdb.managers.query import QueryManager
|
77
|
+
query_mgr = QueryManager(db.project_dir, db.database, db.branch, tenant)
|
78
|
+
affected_rows = query_mgr.execute_non_query(query_sql)
|
79
|
+
console.print(
|
80
|
+
f"[green]✅ Query executed successfully[/green]\n"
|
81
|
+
f"[cyan]Rows affected: {affected_rows}[/cyan]"
|
82
|
+
)
|
83
|
+
else:
|
84
|
+
# For remote connections, the API should handle all SQL types
|
85
|
+
# This might need API support - for now, try using query
|
86
|
+
try:
|
87
|
+
result = db.query(query_sql)
|
88
|
+
console.print(
|
89
|
+
f"[green]✅ Query executed successfully[/green]"
|
90
|
+
)
|
91
|
+
except Exception as e:
|
92
|
+
# If remote doesn't support non-SELECT via query, show helpful message
|
93
|
+
console.print(
|
94
|
+
f"[red]❌ Remote execution of non-SELECT queries may require API support[/red]\n"
|
95
|
+
f"[yellow]Error: {e}[/yellow]"
|
96
|
+
)
|
97
|
+
raise
|
98
|
+
|
99
|
+
except Exception as e:
|
100
|
+
console.print(f"[red]❌ Query error: {e}[/red]")
|
101
|
+
raise typer.Exit(1)
|
102
|
+
|
103
|
+
|
104
|
+
@app.callback()
|
105
|
+
def main(
|
106
|
+
ctx: typer.Context,
|
107
|
+
sql: Optional[str] = typer.Argument(None, help="SQL query to execute"),
|
108
|
+
tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
|
109
|
+
format: Optional[str] = typer.Option(
|
110
|
+
"table", "--format", "-f", help="Output format (table, json, csv)"
|
111
|
+
),
|
112
|
+
limit: Optional[int] = typer.Option(
|
113
|
+
None, "--limit", "-l", help="Limit number of rows"
|
114
|
+
),
|
115
|
+
local: bool = typer.Option(
|
116
|
+
False, "--local", "-L", help="Force local connection"
|
117
|
+
),
|
118
|
+
remote: Optional[str] = typer.Option(
|
119
|
+
None, "--remote", "-r", help="Use specific remote alias"
|
120
|
+
),
|
121
|
+
):
|
122
|
+
"""Execute a SQL query.
|
123
|
+
|
124
|
+
Examples:
|
125
|
+
cinch query "SELECT * FROM users"
|
126
|
+
cinch query "SELECT * FROM users WHERE active = 1" --format json
|
127
|
+
cinch query "SELECT COUNT(*) FROM posts" --tenant tenant1
|
128
|
+
cinch query "SELECT * FROM users" --remote production
|
129
|
+
cinch query "SELECT * FROM users" --local
|
130
|
+
"""
|
131
|
+
# If no subcommand is invoked and we have SQL, execute it
|
132
|
+
if ctx.invoked_subcommand is None:
|
133
|
+
if not sql:
|
134
|
+
console.print(ctx.get_help())
|
135
|
+
raise typer.Exit(0)
|
136
|
+
execute_query(sql, tenant, format, limit, force_local=local, remote_alias=remote)
|