cinchdb 0.1.3__tar.gz → 0.1.5__tar.gz
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-0.1.3 → cinchdb-0.1.5}/.gitignore +3 -1
- {cinchdb-0.1.3 → cinchdb-0.1.5}/PKG-INFO +2 -2
- {cinchdb-0.1.3 → cinchdb-0.1.5}/README.md +1 -1
- {cinchdb-0.1.3 → cinchdb-0.1.5}/pyproject.toml +1 -2
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/branch.py +22 -13
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/column.py +27 -14
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/database.py +2 -2
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/query.py +19 -12
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/remote.py +32 -28
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/table.py +20 -16
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/tenant.py +4 -4
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/main.py +21 -11
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/utils.py +8 -6
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/config.py +18 -45
- cinchdb-0.1.5/src/cinchdb/core/__init__.py +6 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/core/database.py +25 -7
- cinchdb-0.1.5/src/cinchdb/core/initializer.py +214 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/branch.py +1 -3
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/column.py +13 -9
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/data.py +38 -17
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/query.py +12 -6
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/table.py +12 -9
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/tenant.py +3 -3
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/branch.py +1 -1
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/database.py +1 -1
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/table.py +5 -8
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/tenant.py +1 -1
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/utils/__init__.py +5 -5
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/utils/name_validator.py +61 -30
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/utils/sql_validator.py +91 -41
- cinchdb-0.1.3/src/cinchdb/core/__init__.py +0 -5
- {cinchdb-0.1.3 → cinchdb-0.1.5}/LICENSE +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/__main__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/codegen.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/commands/view.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/handlers/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/core/connection.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/core/maintenance.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/core/path_utils.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/change_applier.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/change_comparator.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/change_tracker.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/codegen.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/merge_manager.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/managers/view.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/__init__.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/base.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/change.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/project.py +0 -0
- {cinchdb-0.1.3 → cinchdb-0.1.5}/src/cinchdb/models/view.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cinchdb
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: A Git-like SQLite database management system with branching and multi-tenancy
|
5
5
|
Project-URL: Homepage, https://github.com/russellromney/cinchdb
|
6
6
|
Project-URL: Documentation, https://russellromney.github.io/cinchdb
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
31
31
|
|
32
32
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
33
33
|
|
34
|
-
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database.
|
34
|
+
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database that makes it easy to merge changes between branches.
|
35
35
|
|
36
36
|
On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
37
37
|
|
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
6
6
|
|
7
|
-
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database.
|
7
|
+
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database that makes it easy to merge changes between branches.
|
8
8
|
|
9
9
|
On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
10
10
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "cinchdb"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.5"
|
4
4
|
description = "A Git-like SQLite database management system with branching and multi-tenancy"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -51,7 +51,6 @@ include = [
|
|
51
51
|
|
52
52
|
# Explicitly exclude other directories
|
53
53
|
exclude = [
|
54
|
-
"frontend/",
|
55
54
|
"docs/",
|
56
55
|
"sdk/",
|
57
56
|
"site/",
|
@@ -81,14 +81,14 @@ def create(
|
|
81
81
|
):
|
82
82
|
"""Create a new branch."""
|
83
83
|
name = validate_required_arg(name, "name", ctx)
|
84
|
-
|
84
|
+
|
85
85
|
# Validate branch name
|
86
86
|
try:
|
87
87
|
validate_name(name, "branch")
|
88
88
|
except InvalidNameError as e:
|
89
89
|
console.print(f"[red]❌ {e}[/red]")
|
90
90
|
raise typer.Exit(1)
|
91
|
-
|
91
|
+
|
92
92
|
config, config_data = get_config_with_data()
|
93
93
|
db_name = config_data.active_database
|
94
94
|
source_branch = source or config_data.active_branch
|
@@ -338,24 +338,25 @@ def changes(
|
|
338
338
|
|
339
339
|
try:
|
340
340
|
from cinchdb.managers.change_tracker import ChangeTracker
|
341
|
-
|
341
|
+
|
342
342
|
tracker = ChangeTracker(config.project_dir, db_name, branch_name)
|
343
343
|
changes = tracker.get_changes()
|
344
344
|
|
345
345
|
if not changes:
|
346
|
-
console.print(
|
346
|
+
console.print(
|
347
|
+
f"[yellow]No changes found in branch '{branch_name}'[/yellow]"
|
348
|
+
)
|
347
349
|
return
|
348
350
|
|
349
351
|
if format == "json":
|
350
352
|
# JSON output
|
351
353
|
import json
|
352
|
-
|
353
|
-
|
354
|
+
|
354
355
|
changes_data = []
|
355
356
|
for change in changes:
|
356
|
-
change_dict = change.model_dump(mode=
|
357
|
+
change_dict = change.model_dump(mode="json")
|
357
358
|
changes_data.append(change_dict)
|
358
|
-
|
359
|
+
|
359
360
|
console.print(json.dumps(changes_data, indent=2, default=str))
|
360
361
|
else:
|
361
362
|
# Table output
|
@@ -368,24 +369,32 @@ def changes(
|
|
368
369
|
table.add_column("Created", style="dim")
|
369
370
|
|
370
371
|
for change in changes:
|
371
|
-
created_at =
|
372
|
+
created_at = (
|
373
|
+
change.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
374
|
+
if change.created_at
|
375
|
+
else "Unknown"
|
376
|
+
)
|
372
377
|
applied_status = "✓" if change.applied else "✗"
|
373
378
|
table.add_row(
|
374
379
|
change.id[:8] if change.id else "Unknown",
|
375
|
-
change.type.value
|
380
|
+
change.type.value
|
381
|
+
if hasattr(change.type, "value")
|
382
|
+
else str(change.type),
|
376
383
|
change.entity_name,
|
377
384
|
change.entity_type,
|
378
385
|
applied_status,
|
379
|
-
created_at
|
386
|
+
created_at,
|
380
387
|
)
|
381
388
|
|
382
389
|
console.print(table)
|
383
|
-
|
390
|
+
|
384
391
|
# Summary
|
385
392
|
total = len(changes)
|
386
393
|
applied = sum(1 for c in changes if c.applied)
|
387
394
|
unapplied = total - applied
|
388
|
-
console.print(
|
395
|
+
console.print(
|
396
|
+
f"\n[bold]Total:[/bold] {total} changes ({applied} applied, {unapplied} unapplied)"
|
397
|
+
)
|
389
398
|
|
390
399
|
except ValueError as e:
|
391
400
|
console.print(f"[red]❌ {e}[/red]")
|
@@ -235,7 +235,10 @@ def alter_nullable(
|
|
235
235
|
None, "--nullable/--not-nullable", help="Make column nullable or NOT NULL"
|
236
236
|
),
|
237
237
|
fill_value: Optional[str] = typer.Option(
|
238
|
-
None,
|
238
|
+
None,
|
239
|
+
"--fill-value",
|
240
|
+
"-f",
|
241
|
+
help="Value to use for NULL values when making NOT NULL",
|
239
242
|
),
|
240
243
|
apply: bool = typer.Option(
|
241
244
|
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
@@ -244,20 +247,22 @@ def alter_nullable(
|
|
244
247
|
"""Change the nullable constraint on a column."""
|
245
248
|
table = validate_required_arg(table, "table", ctx)
|
246
249
|
column = validate_required_arg(column, "column", ctx)
|
247
|
-
|
250
|
+
|
248
251
|
# Validate nullable flag was provided
|
249
252
|
if nullable is None:
|
250
253
|
console.print(ctx.get_help())
|
251
|
-
console.print(
|
254
|
+
console.print(
|
255
|
+
"\n[red]❌ Error: Must specify either --nullable or --not-nullable[/red]"
|
256
|
+
)
|
252
257
|
raise typer.Exit(1)
|
253
|
-
|
258
|
+
|
254
259
|
config, config_data = get_config_with_data()
|
255
260
|
db_name = config_data.active_database
|
256
261
|
branch_name = config_data.active_branch
|
257
262
|
|
258
263
|
try:
|
259
264
|
column_mgr = ColumnManager(config.project_dir, db_name, branch_name, "main")
|
260
|
-
|
265
|
+
|
261
266
|
# Check if column has NULLs when making NOT NULL
|
262
267
|
if not nullable and fill_value is None:
|
263
268
|
# Get column info to check current state
|
@@ -266,18 +271,22 @@ def alter_nullable(
|
|
266
271
|
# Check for NULL values
|
267
272
|
from cinchdb.core.connection import DatabaseConnection
|
268
273
|
from cinchdb.core.path_utils import get_tenant_db_path
|
269
|
-
|
270
|
-
db_path = get_tenant_db_path(
|
274
|
+
|
275
|
+
db_path = get_tenant_db_path(
|
276
|
+
config.project_dir, db_name, branch_name, "main"
|
277
|
+
)
|
271
278
|
with DatabaseConnection(db_path) as conn:
|
272
279
|
cursor = conn.execute(
|
273
280
|
f"SELECT COUNT(*) FROM {table} WHERE {column} IS NULL"
|
274
281
|
)
|
275
282
|
null_count = cursor.fetchone()[0]
|
276
|
-
|
283
|
+
|
277
284
|
if null_count > 0:
|
278
|
-
console.print(
|
285
|
+
console.print(
|
286
|
+
f"[yellow]Column '{column}' has {null_count} NULL values.[/yellow]"
|
287
|
+
)
|
279
288
|
fill_value = typer.prompt("Provide a fill value")
|
280
|
-
|
289
|
+
|
281
290
|
# Convert fill_value to appropriate type
|
282
291
|
if fill_value is not None:
|
283
292
|
# Try to interpret the value
|
@@ -288,13 +297,17 @@ def alter_nullable(
|
|
288
297
|
elif fill_value.replace(".", "", 1).isdigit():
|
289
298
|
fill_value = float(fill_value)
|
290
299
|
# Otherwise keep as string
|
291
|
-
|
300
|
+
|
292
301
|
column_mgr.alter_column_nullable(table, column, nullable, fill_value)
|
293
|
-
|
302
|
+
|
294
303
|
if nullable:
|
295
|
-
console.print(
|
304
|
+
console.print(
|
305
|
+
f"[green]✅ Made column '{column}' nullable in table '{table}'[/green]"
|
306
|
+
)
|
296
307
|
else:
|
297
|
-
console.print(
|
308
|
+
console.print(
|
309
|
+
f"[green]✅ Made column '{column}' NOT NULL in table '{table}'[/green]"
|
310
|
+
)
|
298
311
|
|
299
312
|
if apply:
|
300
313
|
# Apply to all tenants
|
@@ -65,14 +65,14 @@ def create(
|
|
65
65
|
):
|
66
66
|
"""Create a new database."""
|
67
67
|
name = validate_required_arg(name, "name", ctx)
|
68
|
-
|
68
|
+
|
69
69
|
# Validate database name
|
70
70
|
try:
|
71
71
|
validate_name(name, "database")
|
72
72
|
except InvalidNameError as e:
|
73
73
|
console.print(f"[red]❌ {e}[/red]")
|
74
74
|
raise typer.Exit(1)
|
75
|
-
|
75
|
+
|
76
76
|
config, config_data = get_config_with_data()
|
77
77
|
|
78
78
|
# Create database directory structure
|
@@ -5,14 +5,20 @@ from typing import Optional
|
|
5
5
|
from rich.console import Console
|
6
6
|
from rich.table import Table as RichTable
|
7
7
|
|
8
|
-
from cinchdb.cli.utils import
|
9
|
-
from cinchdb.managers.query import QueryManager
|
8
|
+
from cinchdb.cli.utils import get_cinchdb_instance
|
10
9
|
|
11
10
|
app = typer.Typer(help="Execute SQL queries", invoke_without_command=True)
|
12
11
|
console = Console()
|
13
12
|
|
14
13
|
|
15
|
-
def execute_query(
|
14
|
+
def execute_query(
|
15
|
+
sql: str,
|
16
|
+
tenant: str,
|
17
|
+
format: str,
|
18
|
+
limit: Optional[int],
|
19
|
+
force_local: bool = False,
|
20
|
+
remote_alias: Optional[str] = None,
|
21
|
+
):
|
16
22
|
"""Execute a SQL query."""
|
17
23
|
# Add LIMIT if specified
|
18
24
|
query_sql = sql
|
@@ -20,7 +26,9 @@ def execute_query(sql: str, tenant: str, format: str, limit: Optional[int], forc
|
|
20
26
|
query_sql = f"{sql} LIMIT {limit}"
|
21
27
|
|
22
28
|
# Get CinchDB instance (handles local/remote automatically)
|
23
|
-
db = get_cinchdb_instance(
|
29
|
+
db = get_cinchdb_instance(
|
30
|
+
tenant=tenant, force_local=force_local, remote_alias=remote_alias
|
31
|
+
)
|
24
32
|
|
25
33
|
try:
|
26
34
|
# Check if this is a SELECT query
|
@@ -74,6 +82,7 @@ def execute_query(sql: str, tenant: str, format: str, limit: Optional[int], forc
|
|
74
82
|
if db.is_local:
|
75
83
|
# For local connections, use the query manager directly
|
76
84
|
from cinchdb.managers.query import QueryManager
|
85
|
+
|
77
86
|
query_mgr = QueryManager(db.project_dir, db.database, db.branch, tenant)
|
78
87
|
affected_rows = query_mgr.execute_non_query(query_sql)
|
79
88
|
console.print(
|
@@ -84,10 +93,8 @@ def execute_query(sql: str, tenant: str, format: str, limit: Optional[int], forc
|
|
84
93
|
# For remote connections, the API should handle all SQL types
|
85
94
|
# This might need API support - for now, try using query
|
86
95
|
try:
|
87
|
-
|
88
|
-
console.print(
|
89
|
-
f"[green]✅ Query executed successfully[/green]"
|
90
|
-
)
|
96
|
+
db.query(query_sql)
|
97
|
+
console.print("[green]✅ Query executed successfully[/green]")
|
91
98
|
except Exception as e:
|
92
99
|
# If remote doesn't support non-SELECT via query, show helpful message
|
93
100
|
console.print(
|
@@ -112,9 +119,7 @@ def main(
|
|
112
119
|
limit: Optional[int] = typer.Option(
|
113
120
|
None, "--limit", "-l", help="Limit number of rows"
|
114
121
|
),
|
115
|
-
local: bool = typer.Option(
|
116
|
-
False, "--local", "-L", help="Force local connection"
|
117
|
-
),
|
122
|
+
local: bool = typer.Option(False, "--local", "-L", help="Force local connection"),
|
118
123
|
remote: Optional[str] = typer.Option(
|
119
124
|
None, "--remote", "-r", help="Use specific remote alias"
|
120
125
|
),
|
@@ -133,4 +138,6 @@ def main(
|
|
133
138
|
if not sql:
|
134
139
|
console.print(ctx.get_help())
|
135
140
|
raise typer.Exit(0)
|
136
|
-
execute_query(
|
141
|
+
execute_query(
|
142
|
+
sql, tenant, format, limit, force_local=local, remote_alias=remote
|
143
|
+
)
|
@@ -21,25 +21,27 @@ def add_remote(
|
|
21
21
|
):
|
22
22
|
"""Add a remote CinchDB instance configuration."""
|
23
23
|
alias = validate_required_arg(alias, "alias", ctx)
|
24
|
-
|
24
|
+
|
25
25
|
if not url:
|
26
26
|
console.print("[red]❌ Error: --url is required[/red]")
|
27
27
|
raise typer.Exit(1)
|
28
|
-
|
28
|
+
|
29
29
|
if not key:
|
30
30
|
console.print("[red]❌ Error: --key is required[/red]")
|
31
31
|
raise typer.Exit(1)
|
32
|
-
|
32
|
+
|
33
33
|
config, config_data = get_config_with_data()
|
34
|
-
|
34
|
+
|
35
35
|
# Check if alias already exists
|
36
36
|
if alias in config_data.remotes:
|
37
|
-
console.print(
|
38
|
-
|
37
|
+
console.print(
|
38
|
+
f"[yellow]⚠️ Remote '{alias}' already exists. Updating...[/yellow]"
|
39
|
+
)
|
40
|
+
|
39
41
|
# Add or update the remote
|
40
42
|
config_data.remotes[alias] = RemoteConfig(url=url.rstrip("/"), key=key)
|
41
43
|
config.save(config_data)
|
42
|
-
|
44
|
+
|
43
45
|
console.print(f"[green]✓ Remote '{alias}' configured successfully[/green]")
|
44
46
|
|
45
47
|
|
@@ -47,20 +49,20 @@ def add_remote(
|
|
47
49
|
def list_remotes():
|
48
50
|
"""List all configured remote instances."""
|
49
51
|
config, config_data = get_config_with_data()
|
50
|
-
|
52
|
+
|
51
53
|
if not config_data.remotes:
|
52
54
|
console.print("[yellow]No remotes configured[/yellow]")
|
53
55
|
return
|
54
|
-
|
56
|
+
|
55
57
|
table = Table(title="Configured Remotes")
|
56
58
|
table.add_column("Alias", style="cyan")
|
57
59
|
table.add_column("URL", style="green")
|
58
60
|
table.add_column("Active", style="yellow")
|
59
|
-
|
61
|
+
|
60
62
|
for alias, remote in config_data.remotes.items():
|
61
63
|
is_active = "✓" if alias == config_data.active_remote else ""
|
62
64
|
table.add_row(alias, remote.url, is_active)
|
63
|
-
|
65
|
+
|
64
66
|
console.print(table)
|
65
67
|
|
66
68
|
|
@@ -71,20 +73,20 @@ def remove_remote(
|
|
71
73
|
):
|
72
74
|
"""Remove a remote configuration."""
|
73
75
|
alias = validate_required_arg(alias, "alias", ctx)
|
74
|
-
|
76
|
+
|
75
77
|
config, config_data = get_config_with_data()
|
76
|
-
|
78
|
+
|
77
79
|
if alias not in config_data.remotes:
|
78
80
|
console.print(f"[red]❌ Remote '{alias}' not found[/red]")
|
79
81
|
raise typer.Exit(1)
|
80
|
-
|
82
|
+
|
81
83
|
# Remove the remote
|
82
84
|
del config_data.remotes[alias]
|
83
|
-
|
85
|
+
|
84
86
|
# If this was the active remote, clear it
|
85
87
|
if config_data.active_remote == alias:
|
86
88
|
config_data.active_remote = None
|
87
|
-
|
89
|
+
|
88
90
|
config.save(config_data)
|
89
91
|
console.print(f"[green]✓ Remote '{alias}' removed[/green]")
|
90
92
|
|
@@ -96,16 +98,16 @@ def use_remote(
|
|
96
98
|
):
|
97
99
|
"""Set the active remote instance."""
|
98
100
|
alias = validate_required_arg(alias, "alias", ctx)
|
99
|
-
|
101
|
+
|
100
102
|
config, config_data = get_config_with_data()
|
101
|
-
|
103
|
+
|
102
104
|
if alias not in config_data.remotes:
|
103
105
|
console.print(f"[red]❌ Remote '{alias}' not found[/red]")
|
104
106
|
raise typer.Exit(1)
|
105
|
-
|
107
|
+
|
106
108
|
config_data.active_remote = alias
|
107
109
|
config.save(config_data)
|
108
|
-
|
110
|
+
|
109
111
|
console.print(f"[green]✓ Now using remote '{alias}'[/green]")
|
110
112
|
console.print(f"[dim]URL: {config_data.remotes[alias].url}[/dim]")
|
111
113
|
|
@@ -114,14 +116,14 @@ def use_remote(
|
|
114
116
|
def clear_remote():
|
115
117
|
"""Clear the active remote (switch back to local mode)."""
|
116
118
|
config, config_data = get_config_with_data()
|
117
|
-
|
119
|
+
|
118
120
|
if not config_data.active_remote:
|
119
121
|
console.print("[yellow]No active remote set[/yellow]")
|
120
122
|
return
|
121
|
-
|
123
|
+
|
122
124
|
config_data.active_remote = None
|
123
125
|
config.save(config_data)
|
124
|
-
|
126
|
+
|
125
127
|
console.print("[green]✓ Cleared active remote. Now using local mode.[/green]")
|
126
128
|
|
127
129
|
|
@@ -129,16 +131,18 @@ def clear_remote():
|
|
129
131
|
def show_remote():
|
130
132
|
"""Show the currently active remote."""
|
131
133
|
config, config_data = get_config_with_data()
|
132
|
-
|
134
|
+
|
133
135
|
if not config_data.active_remote:
|
134
136
|
console.print("[yellow]No active remote. Using local mode.[/yellow]")
|
135
137
|
return
|
136
|
-
|
138
|
+
|
137
139
|
alias = config_data.active_remote
|
138
140
|
if alias not in config_data.remotes:
|
139
|
-
console.print(
|
141
|
+
console.print(
|
142
|
+
f"[red]❌ Active remote '{alias}' not found in configuration[/red]"
|
143
|
+
)
|
140
144
|
return
|
141
|
-
|
145
|
+
|
142
146
|
remote = config_data.remotes[alias]
|
143
147
|
console.print(f"[green]Active remote:[/green] {alias}")
|
144
|
-
console.print(f"[dim]URL: {remote.url}[/dim]")
|
148
|
+
console.print(f"[dim]URL: {remote.url}[/dim]")
|
@@ -60,7 +60,8 @@ def create(
|
|
60
60
|
ctx: typer.Context,
|
61
61
|
name: Optional[str] = typer.Argument(None, help="Name of the table"),
|
62
62
|
columns: Optional[List[str]] = typer.Argument(
|
63
|
-
None,
|
63
|
+
None,
|
64
|
+
help="Column definitions (format: name:type[:nullable][:fk=table[.column][:action]])",
|
64
65
|
),
|
65
66
|
apply: bool = typer.Option(
|
66
67
|
True, "--apply/--no-apply", help="Apply changes to all tenants"
|
@@ -92,14 +93,16 @@ def create(
|
|
92
93
|
parts = col_def.split(":")
|
93
94
|
if len(parts) < 2:
|
94
95
|
console.print(f"[red]❌ Invalid column definition: '{col_def}'[/red]")
|
95
|
-
console.print(
|
96
|
+
console.print(
|
97
|
+
"[yellow]Format: name:type[:nullable][:fk=table[.column][:action]][/yellow]"
|
98
|
+
)
|
96
99
|
raise typer.Exit(1)
|
97
100
|
|
98
101
|
col_name = parts[0]
|
99
102
|
col_type = parts[1].upper()
|
100
103
|
nullable = False
|
101
104
|
foreign_key = None
|
102
|
-
|
105
|
+
|
103
106
|
# Parse additional parts
|
104
107
|
for i in range(2, len(parts)):
|
105
108
|
part = parts[i]
|
@@ -108,7 +111,7 @@ def create(
|
|
108
111
|
elif part.startswith("fk="):
|
109
112
|
# Parse foreign key definition
|
110
113
|
fk_def = part[3:] # Remove "fk=" prefix
|
111
|
-
|
114
|
+
|
112
115
|
# Handle actions with spaces (e.g., "set null", "no action")
|
113
116
|
# Check for known actions at the end
|
114
117
|
fk_action = "RESTRICT" # Default
|
@@ -116,12 +119,12 @@ def create(
|
|
116
119
|
if fk_def.lower().endswith("." + action):
|
117
120
|
fk_action = action.upper()
|
118
121
|
# Remove the action part from fk_def
|
119
|
-
fk_def = fk_def[
|
122
|
+
fk_def = fk_def[: -len("." + action)]
|
120
123
|
break
|
121
|
-
|
124
|
+
|
122
125
|
# Now split the remaining parts
|
123
126
|
fk_parts = fk_def.split(".")
|
124
|
-
|
127
|
+
|
125
128
|
if len(fk_parts) == 1:
|
126
129
|
# Just table name, column defaults to "id"
|
127
130
|
fk_table = fk_parts[0]
|
@@ -131,15 +134,17 @@ def create(
|
|
131
134
|
fk_table = fk_parts[0]
|
132
135
|
fk_column = fk_parts[1]
|
133
136
|
else:
|
134
|
-
console.print(
|
137
|
+
console.print(
|
138
|
+
f"[red]❌ Invalid foreign key format: '{fk_def}'[/red]"
|
139
|
+
)
|
135
140
|
console.print("[yellow]Format: fk=table[.column][:action][/yellow]")
|
136
141
|
raise typer.Exit(1)
|
137
|
-
|
142
|
+
|
138
143
|
foreign_key = ForeignKeyRef(
|
139
144
|
table=fk_table,
|
140
145
|
column=fk_column,
|
141
146
|
on_delete=fk_action,
|
142
|
-
on_update="RESTRICT" # Default to RESTRICT for updates
|
147
|
+
on_update="RESTRICT", # Default to RESTRICT for updates
|
143
148
|
)
|
144
149
|
|
145
150
|
if col_type not in ["TEXT", "INTEGER", "REAL", "BLOB", "NUMERIC"]:
|
@@ -149,12 +154,11 @@ def create(
|
|
149
154
|
)
|
150
155
|
raise typer.Exit(1)
|
151
156
|
|
152
|
-
parsed_columns.append(
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
))
|
157
|
+
parsed_columns.append(
|
158
|
+
Column(
|
159
|
+
name=col_name, type=col_type, nullable=nullable, foreign_key=foreign_key
|
160
|
+
)
|
161
|
+
)
|
158
162
|
|
159
163
|
try:
|
160
164
|
table_mgr = TableManager(config.project_dir, db_name, branch_name, "main")
|
@@ -71,14 +71,14 @@ def create(
|
|
71
71
|
):
|
72
72
|
"""Create a new tenant."""
|
73
73
|
name = validate_required_arg(name, "name", ctx)
|
74
|
-
|
74
|
+
|
75
75
|
# Validate tenant name
|
76
76
|
try:
|
77
77
|
validate_name(name, "tenant")
|
78
78
|
except InvalidNameError as e:
|
79
79
|
console.print(f"[red]❌ {e}[/red]")
|
80
80
|
raise typer.Exit(1)
|
81
|
-
|
81
|
+
|
82
82
|
config, config_data = get_config_with_data()
|
83
83
|
db_name = config_data.active_database
|
84
84
|
branch_name = config_data.active_branch
|
@@ -164,14 +164,14 @@ def rename(
|
|
164
164
|
"""Rename a tenant."""
|
165
165
|
old_name = validate_required_arg(old_name, "old_name", ctx)
|
166
166
|
new_name = validate_required_arg(new_name, "new_name", ctx)
|
167
|
-
|
167
|
+
|
168
168
|
# Validate new tenant name
|
169
169
|
try:
|
170
170
|
validate_name(new_name, "tenant")
|
171
171
|
except InvalidNameError as e:
|
172
172
|
console.print(f"[red]❌ {e}[/red]")
|
173
173
|
raise typer.Exit(1)
|
174
|
-
|
174
|
+
|
175
175
|
config, config_data = get_config_with_data()
|
176
176
|
db_name = config_data.active_database
|
177
177
|
branch_name = config_data.active_branch
|
@@ -73,18 +73,27 @@ def init(
|
|
73
73
|
path: Optional[Path] = typer.Argument(
|
74
74
|
None, help="Directory to initialize project in (default: current directory)"
|
75
75
|
),
|
76
|
+
database: Optional[str] = typer.Option(
|
77
|
+
"main", "--database", "-d", help="Initial database name"
|
78
|
+
),
|
79
|
+
branch: Optional[str] = typer.Option(
|
80
|
+
"main", "--branch", "-b", help="Initial branch name"
|
81
|
+
),
|
76
82
|
):
|
77
83
|
"""Initialize a new CinchDB project."""
|
78
|
-
from cinchdb.
|
84
|
+
from cinchdb.core.initializer import init_project
|
79
85
|
|
80
86
|
project_path = path or Path.cwd()
|
81
87
|
|
82
88
|
try:
|
83
|
-
|
84
|
-
|
89
|
+
# Use the core initializer directly
|
90
|
+
init_project(
|
91
|
+
project_dir=project_path, database_name=database, branch_name=branch
|
92
|
+
)
|
85
93
|
typer.secho(
|
86
94
|
f"✅ Initialized CinchDB project in {project_path}", fg=typer.colors.GREEN
|
87
95
|
)
|
96
|
+
typer.secho(f" Database: {database}, Branch: {branch}", fg=typer.colors.CYAN)
|
88
97
|
except FileExistsError:
|
89
98
|
typer.secho(f"❌ Project already exists in {project_path}", fg=typer.colors.RED)
|
90
99
|
raise typer.Exit(1)
|
@@ -103,31 +112,32 @@ def status():
|
|
103
112
|
"""Show CinchDB status including configuration and environment variables."""
|
104
113
|
from cinchdb.cli.utils import get_config_with_data, show_env_config
|
105
114
|
from rich.console import Console
|
106
|
-
|
107
|
-
|
115
|
+
|
108
116
|
console = Console()
|
109
|
-
|
117
|
+
|
110
118
|
# Show project configuration
|
111
119
|
try:
|
112
120
|
config, config_data = get_config_with_data()
|
113
|
-
|
121
|
+
|
114
122
|
console.print("\n[bold]CinchDB Status[/bold]")
|
115
123
|
console.print(f"Project: {config.project_dir}")
|
116
124
|
console.print(f"Active Database: {config_data.active_database}")
|
117
125
|
console.print(f"Active Branch: {config_data.active_branch}")
|
118
|
-
|
126
|
+
|
119
127
|
if config_data.active_remote:
|
120
128
|
console.print(f"Active Remote: {config_data.active_remote}")
|
121
129
|
if config_data.active_remote in config_data.remotes:
|
122
130
|
remote = config_data.remotes[config_data.active_remote]
|
123
131
|
console.print(f" URL: {remote.url}")
|
124
|
-
console.print(
|
132
|
+
console.print(
|
133
|
+
f" Key: ***{remote.key[-8:] if len(remote.key) > 8 else '*' * len(remote.key)}"
|
134
|
+
)
|
125
135
|
else:
|
126
136
|
console.print("Active Remote: [dim]None (local mode)[/dim]")
|
127
|
-
|
137
|
+
|
128
138
|
# Show environment variables
|
129
139
|
show_env_config()
|
130
|
-
|
140
|
+
|
131
141
|
except Exception as e:
|
132
142
|
console.print(f"[red]❌ Error: {e}[/red]")
|
133
143
|
raise typer.Exit(1)
|