cinchdb 0.1.10__tar.gz → 0.1.12__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.
Files changed (61) hide show
  1. {cinchdb-0.1.10 → cinchdb-0.1.12}/PKG-INFO +10 -37
  2. {cinchdb-0.1.10 → cinchdb-0.1.12}/README.md +9 -36
  3. {cinchdb-0.1.10 → cinchdb-0.1.12}/pyproject.toml +1 -1
  4. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/column.py +3 -4
  5. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/database.py +58 -60
  6. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/table.py +3 -3
  7. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/main.py +1 -7
  8. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/utils.py +23 -0
  9. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/core/database.py +138 -11
  10. cinchdb-0.1.12/src/cinchdb/core/initializer.py +392 -0
  11. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/core/path_utils.py +44 -27
  12. cinchdb-0.1.12/src/cinchdb/infrastructure/metadata_connection_pool.py +145 -0
  13. cinchdb-0.1.12/src/cinchdb/infrastructure/metadata_db.py +376 -0
  14. cinchdb-0.1.12/src/cinchdb/managers/branch.py +266 -0
  15. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/change_applier.py +30 -13
  16. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/column.py +4 -10
  17. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/query.py +40 -4
  18. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/table.py +8 -6
  19. cinchdb-0.1.12/src/cinchdb/managers/tenant.py +901 -0
  20. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/table.py +0 -4
  21. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/tenant.py +4 -2
  22. cinchdb-0.1.10/src/cinchdb/core/initializer.py +0 -214
  23. cinchdb-0.1.10/src/cinchdb/managers/branch.py +0 -170
  24. cinchdb-0.1.10/src/cinchdb/managers/tenant.py +0 -370
  25. {cinchdb-0.1.10 → cinchdb-0.1.12}/.gitignore +0 -0
  26. {cinchdb-0.1.10 → cinchdb-0.1.12}/LICENSE +0 -0
  27. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/__init__.py +0 -0
  28. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/__main__.py +0 -0
  29. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/__init__.py +0 -0
  30. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/__init__.py +0 -0
  31. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/branch.py +0 -0
  32. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/codegen.py +0 -0
  33. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/index.py +0 -0
  34. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/query.py +0 -0
  35. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/remote.py +0 -0
  36. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/tenant.py +0 -0
  37. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/commands/view.py +0 -0
  38. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/handlers/__init__.py +0 -0
  39. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
  40. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/config.py +0 -0
  41. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/core/__init__.py +0 -0
  42. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/core/connection.py +0 -0
  43. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/core/maintenance.py +0 -0
  44. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/__init__.py +0 -0
  45. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/change_comparator.py +0 -0
  46. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/change_tracker.py +0 -0
  47. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/codegen.py +0 -0
  48. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/data.py +0 -0
  49. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/index.py +0 -0
  50. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/merge_manager.py +0 -0
  51. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/managers/view.py +0 -0
  52. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/__init__.py +0 -0
  53. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/base.py +0 -0
  54. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/branch.py +0 -0
  55. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/change.py +0 -0
  56. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/database.py +0 -0
  57. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/project.py +0 -0
  58. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/models/view.py +0 -0
  59. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/utils/__init__.py +0 -0
  60. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/utils/name_validator.py +0 -0
  61. {cinchdb-0.1.10 → cinchdb-0.1.12}/src/cinchdb/utils/sql_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cinchdb
3
- Version: 0.1.10
3
+ Version: 0.1.12
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
@@ -39,7 +39,11 @@ Because it's so lightweight and its only dependencies are pydantic, requests, an
39
39
 
40
40
 
41
41
  ```bash
42
- uv pip install cinchdb
42
+ # Recommended: Install with uv (faster, better dependency resolution)
43
+ uv add cinchdb
44
+
45
+ # Or with pip
46
+ pip install cinchdb
43
47
 
44
48
  # Initialize project
45
49
  cinch init
@@ -58,10 +62,7 @@ cinch branch merge-into-main feature
58
62
  cinch tenant create customer_a
59
63
  cinch query "SELECT * FROM users" --tenant customer_a
60
64
 
61
- # Coming soon
62
- # Connect to remote CinchDB instance
63
- cinch remote add production https://your-cinchdb-server.com your-api-key
64
- cinch remote use production
65
+ # Future: Remote connectivity planned for production deployment
65
66
 
66
67
  # Autogenerate Python SDK from database
67
68
  cinch codegen generate python cinchdb_models/
@@ -75,9 +76,7 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
75
76
  - **Multi-tenant isolation** - shared schema, isolated data per tenant
76
77
  - **Automatic change tracking** - all schema changes tracked and mergeable
77
78
  - **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
78
- - **Remote connectivity** - Connect to hosted CinchDB instances
79
- - **Type-safe SDK** - Python and TypeScript SDKs with full type safety
80
- - **Remote-capable** - coming soon - CLI and SDK can connect to remote instances
79
+ - **Type-safe Python SDK** - Python SDK with full type safety
81
80
  - **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
82
81
 
83
82
  ## Installation
@@ -149,37 +148,11 @@ results = db.insert("posts", *post_list)
149
148
  db.update("posts", post_id, {"content": "Updated content"})
150
149
  ```
151
150
 
152
- ### Remote Connection
153
-
154
- Coming soon.
155
-
156
- ```python
157
- # Connect to remote instance
158
- db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
159
-
160
- # Same interface as local
161
- results = db.query("SELECT * FROM users")
162
- user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
163
- ```
164
-
165
- ## Remote Access - coming soon
166
-
167
- Connect to a remote CinchDB instance:
168
-
169
- ```bash
170
- cinch remote add production https://your-cinchdb-server.com your-api-key
171
- cinch remote use production
172
- # Now all commands will use the remote instance
173
- ```
174
-
175
- Interactive docs at `/docs`, health check at `/health`.
176
151
 
177
152
  ## Architecture
178
153
 
179
- - **Python SDK**: Core functionality (local + remote)
180
- - **CLI**: Full-featured command-line interface
181
- - **Remote Access**: Connect to hosted CinchDB instances
182
- - **TypeScript SDK**: Browser and Node.js client
154
+ - **Python SDK**: Core functionality for local development
155
+ - **CLI**: Full-featured command-line interface
183
156
 
184
157
  ## Development
185
158
 
@@ -12,7 +12,11 @@ Because it's so lightweight and its only dependencies are pydantic, requests, an
12
12
 
13
13
 
14
14
  ```bash
15
- uv pip install cinchdb
15
+ # Recommended: Install with uv (faster, better dependency resolution)
16
+ uv add cinchdb
17
+
18
+ # Or with pip
19
+ pip install cinchdb
16
20
 
17
21
  # Initialize project
18
22
  cinch init
@@ -31,10 +35,7 @@ cinch branch merge-into-main feature
31
35
  cinch tenant create customer_a
32
36
  cinch query "SELECT * FROM users" --tenant customer_a
33
37
 
34
- # Coming soon
35
- # Connect to remote CinchDB instance
36
- cinch remote add production https://your-cinchdb-server.com your-api-key
37
- cinch remote use production
38
+ # Future: Remote connectivity planned for production deployment
38
39
 
39
40
  # Autogenerate Python SDK from database
40
41
  cinch codegen generate python cinchdb_models/
@@ -48,9 +49,7 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
48
49
  - **Multi-tenant isolation** - shared schema, isolated data per tenant
49
50
  - **Automatic change tracking** - all schema changes tracked and mergeable
50
51
  - **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
51
- - **Remote connectivity** - Connect to hosted CinchDB instances
52
- - **Type-safe SDK** - Python and TypeScript SDKs with full type safety
53
- - **Remote-capable** - coming soon - CLI and SDK can connect to remote instances
52
+ - **Type-safe Python SDK** - Python SDK with full type safety
54
53
  - **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
55
54
 
56
55
  ## Installation
@@ -122,37 +121,11 @@ results = db.insert("posts", *post_list)
122
121
  db.update("posts", post_id, {"content": "Updated content"})
123
122
  ```
124
123
 
125
- ### Remote Connection
126
-
127
- Coming soon.
128
-
129
- ```python
130
- # Connect to remote instance
131
- db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
132
-
133
- # Same interface as local
134
- results = db.query("SELECT * FROM users")
135
- user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
136
- ```
137
-
138
- ## Remote Access - coming soon
139
-
140
- Connect to a remote CinchDB instance:
141
-
142
- ```bash
143
- cinch remote add production https://your-cinchdb-server.com your-api-key
144
- cinch remote use production
145
- # Now all commands will use the remote instance
146
- ```
147
-
148
- Interactive docs at `/docs`, health check at `/health`.
149
124
 
150
125
  ## Architecture
151
126
 
152
- - **Python SDK**: Core functionality (local + remote)
153
- - **CLI**: Full-featured command-line interface
154
- - **Remote Access**: Connect to hosted CinchDB instances
155
- - **TypeScript SDK**: Browser and Node.js client
127
+ - **Python SDK**: Core functionality for local development
128
+ - **CLI**: Full-featured command-line interface
156
129
 
157
130
  ## Development
158
131
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinchdb"
3
- version = "0.1.10"
3
+ version = "0.1.12"
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"
@@ -41,14 +41,14 @@ def list_columns(
41
41
  col_table.add_column("Name", style="cyan")
42
42
  col_table.add_column("Type", style="green")
43
43
  col_table.add_column("Nullable", style="yellow")
44
- col_table.add_column("Primary Key", style="red")
44
+ col_table.add_column("Unique", style="red")
45
45
  col_table.add_column("Default", style="blue")
46
46
 
47
47
  for col in columns:
48
48
  nullable = "Yes" if col.nullable else "No"
49
- pk = "Yes" if col.primary_key else "No"
49
+ unique = "Yes" if col.unique else "No"
50
50
  default = col.default or "-"
51
- col_table.add_row(col.name, col.type, nullable, pk, default)
51
+ col_table.add_row(col.name, col.type, nullable, unique, default)
52
52
 
53
53
  console.print(col_table)
54
54
 
@@ -217,7 +217,6 @@ def info(
217
217
  console.print(f"Table: {table}")
218
218
  console.print(f"Type: {column.type}")
219
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
220
  console.print(f"Unique: {'Yes' if column.unique else 'No'}")
222
221
  console.print(f"Default: {column.default or 'None'}")
223
222
 
@@ -75,43 +75,18 @@ def create(
75
75
 
76
76
  config, config_data = get_config_with_data()
77
77
 
78
- # Create database directory structure
79
- db_path = config.project_dir / ".cinchdb" / "databases" / name
80
- if db_path.exists():
78
+ # Use the ProjectInitializer to create the database properly
79
+ from cinchdb.core.initializer import ProjectInitializer
80
+
81
+ initializer = ProjectInitializer(config.project_dir)
82
+
83
+ try:
84
+ # Create database using initializer (lazy by default)
85
+ initializer.init_database(name, description=description, lazy=True)
86
+ except FileExistsError:
81
87
  console.print(f"[red]❌ Database '{name}' already exists[/red]")
82
88
  raise typer.Exit(1)
83
89
 
84
- # Create the database structure
85
- db_path.mkdir(parents=True)
86
- branches_dir = db_path / "branches"
87
- branches_dir.mkdir()
88
-
89
- # Create main branch
90
- main_branch = branches_dir / "main"
91
- main_branch.mkdir()
92
-
93
- # Create main tenant
94
- tenants_dir = main_branch / "tenants"
95
- tenants_dir.mkdir()
96
- main_tenant = tenants_dir / "main.db"
97
- main_tenant.touch()
98
-
99
- # Create branch metadata
100
- import json
101
- from datetime import datetime, timezone
102
-
103
- metadata = {
104
- "name": "main",
105
- "parent": None,
106
- "created_at": datetime.now(timezone.utc).isoformat(),
107
- }
108
- with open(main_branch / "metadata.json", "w") as f:
109
- json.dump(metadata, f, indent=2)
110
-
111
- # Create empty changes file
112
- with open(main_branch / "changes.json", "w") as f:
113
- json.dump([], f)
114
-
115
90
  console.print(f"[green]✅ Created database '{name}'[/green]")
116
91
 
117
92
  if switch:
@@ -134,11 +109,15 @@ def delete(
134
109
  raise typer.Exit(1)
135
110
 
136
111
  config, config_data = get_config_with_data()
137
- db_path = config.project_dir / ".cinchdb" / "databases" / name
138
-
139
- if not db_path.exists():
140
- console.print(f"[red]❌ Database '{name}' does not exist[/red]")
141
- raise typer.Exit(1)
112
+
113
+ # Check if database exists in metadata
114
+ from cinchdb.infrastructure.metadata_db import MetadataDB
115
+
116
+ with MetadataDB(config.project_dir) as metadata_db:
117
+ db_info = metadata_db.get_database(name)
118
+ if not db_info:
119
+ console.print(f"[red]❌ Database '{name}' does not exist[/red]")
120
+ raise typer.Exit(1)
142
121
 
143
122
  # Confirmation
144
123
  if not force:
@@ -147,10 +126,17 @@ def delete(
147
126
  console.print("[yellow]Cancelled[/yellow]")
148
127
  raise typer.Exit(0)
149
128
 
150
- # Delete the database
129
+ # Delete the database from metadata and filesystem if materialized
151
130
  import shutil
152
-
153
- shutil.rmtree(db_path)
131
+
132
+ with MetadataDB(config.project_dir) as metadata_db:
133
+ # Delete from metadata
134
+ metadata_db.delete_database(db_info['id'])
135
+
136
+ # Delete physical files if they exist
137
+ db_path = config.project_dir / ".cinchdb" / "databases" / name
138
+ if db_path.exists():
139
+ shutil.rmtree(db_path)
154
140
 
155
141
  # If this was the active database, switch to main
156
142
  if config_data.active_database == name:
@@ -168,31 +154,40 @@ def info(
168
154
  config, config_data = get_config_with_data()
169
155
  db_name = name or config_data.active_database
170
156
 
171
- db_path = config.project_dir / ".cinchdb" / "databases" / db_name
172
- if not db_path.exists():
173
- console.print(f"[red]❌ Database '{db_name}' does not exist[/red]")
174
- raise typer.Exit(1)
175
-
176
- # Count branches
177
- branches_path = db_path / "branches"
178
- branch_count = len(list(branches_path.iterdir())) if branches_path.exists() else 0
157
+ # Check if database exists in metadata
158
+ from cinchdb.infrastructure.metadata_db import MetadataDB
159
+
160
+ with MetadataDB(config.project_dir) as metadata_db:
161
+ db_info = metadata_db.get_database(db_name)
162
+ if not db_info:
163
+ console.print(f"[red]❌ Database '{db_name}' does not exist[/red]")
164
+ raise typer.Exit(1)
165
+
166
+ # Get branch info from metadata
167
+ branches = metadata_db.list_branches(db_info['id'])
168
+ branch_count = len(branches)
179
169
 
180
170
  # Get active branch
181
171
  active_branch = config_data.active_branch
182
-
172
+
183
173
  # Display info
184
174
  console.print(f"\n[bold]Database: {db_name}[/bold]")
185
- console.print(f"Location: {db_path}")
175
+ console.print(f"Status: {'Materialized' if db_info.get('materialized') else 'Lazy (not materialized)'}")
186
176
  console.print(f"Branches: {branch_count}")
187
177
  console.print(f"Active Branch: {active_branch}")
188
178
  console.print(f"Protected: {'Yes' if db_name == 'main' else 'No'}")
179
+
180
+ # Show description if present
181
+ if db_info.get('description'):
182
+ console.print(f"Description: {db_info['description']}")
189
183
 
190
184
  # List branches
191
185
  if branch_count > 0:
192
186
  console.print("\n[bold]Branches:[/bold]")
193
- for branch_dir in sorted(branches_path.iterdir()):
194
- if branch_dir.is_dir():
195
- branch_name = branch_dir.name
187
+ with MetadataDB(config.project_dir) as metadata_db:
188
+ branches = metadata_db.list_branches(db_info['id'])
189
+ for branch in sorted(branches, key=lambda x: x['name']):
190
+ branch_name = branch['name']
196
191
  is_active = " (active)" if branch_name == active_branch else ""
197
192
  console.print(f" - {branch_name}{is_active}")
198
193
 
@@ -208,11 +203,14 @@ def switch(
208
203
  name = validate_required_arg(name, "name", ctx)
209
204
  config, config_data = get_config_with_data()
210
205
 
211
- # Check if database exists
212
- db_path = config.project_dir / ".cinchdb" / "databases" / name
213
- if not db_path.exists():
214
- console.print(f"[red]❌ Database '{name}' does not exist[/red]")
215
- raise typer.Exit(1)
206
+ # Check if database exists in metadata
207
+ from cinchdb.infrastructure.metadata_db import MetadataDB
208
+
209
+ with MetadataDB(config.project_dir) as metadata_db:
210
+ db_info = metadata_db.get_database(name)
211
+ if not db_info:
212
+ console.print(f"[red]❌ Database '{name}' does not exist[/red]")
213
+ raise typer.Exit(1)
216
214
 
217
215
  # Switch
218
216
  set_active_database(config, name)
@@ -277,14 +277,14 @@ def info(
277
277
  col_table.add_column("Name", style="cyan")
278
278
  col_table.add_column("Type", style="green")
279
279
  col_table.add_column("Nullable", style="yellow")
280
- col_table.add_column("Primary Key", style="red")
280
+ col_table.add_column("Unique", style="red")
281
281
  col_table.add_column("Default", style="blue")
282
282
 
283
283
  for col in table.columns:
284
284
  nullable = "Yes" if col.nullable else "No"
285
- pk = "Yes" if col.primary_key else "No"
285
+ unique = "Yes" if col.unique else "No"
286
286
  default = col.default or "-"
287
- col_table.add_row(col.name, col.type, nullable, pk, default)
287
+ col_table.add_row(col.name, col.type, nullable, unique, default)
288
288
 
289
289
  console.print(col_table)
290
290
 
@@ -13,7 +13,6 @@ from cinchdb.cli.commands import (
13
13
  column,
14
14
  view,
15
15
  codegen,
16
- remote,
17
16
  index,
18
17
  )
19
18
 
@@ -45,7 +44,6 @@ app.add_typer(column.app, name="column", help="Column management commands")
45
44
  app.add_typer(view.app, name="view", help="View management commands")
46
45
  app.add_typer(index.app, name="index", help="Index management commands")
47
46
  app.add_typer(codegen.app, name="codegen", help="Code generation commands")
48
- app.add_typer(remote.app, name="remote", help="Remote instance management")
49
47
 
50
48
 
51
49
  # Add query as direct command instead of subtyper
@@ -59,15 +57,11 @@ def query(
59
57
  limit: Optional[int] = typer.Option(
60
58
  None, "--limit", "-l", help="Limit number of rows"
61
59
  ),
62
- local: bool = typer.Option(False, "--local", "-L", help="Force local connection"),
63
- remote: Optional[str] = typer.Option(
64
- None, "--remote", "-r", help="Use specific remote alias"
65
- ),
66
60
  ):
67
61
  """Execute a SQL query."""
68
62
  from cinchdb.cli.commands.query import execute_query
69
63
 
70
- execute_query(sql, tenant, format, limit, force_local=local, remote_alias=remote)
64
+ execute_query(sql, tenant, format, limit)
71
65
 
72
66
 
73
67
  @app.command()
@@ -13,6 +13,29 @@ from cinchdb.core.database import CinchDB
13
13
  console = Console()
14
14
 
15
15
 
16
+ def handle_cli_error(func):
17
+ """Decorator to handle CLI errors with consistent formatting.
18
+
19
+ Args:
20
+ func: The function to wrap
21
+ """
22
+ def wrapper(*args, **kwargs):
23
+ try:
24
+ return func(*args, **kwargs)
25
+ except Exception as error:
26
+ error_msg = str(error)
27
+ if "not found" in error_msg.lower():
28
+ console.print(f"[yellow]⚠️ {error_msg}[/yellow]")
29
+ elif "already exists" in error_msg.lower():
30
+ console.print(f"[yellow]⚠️ {error_msg}[/yellow]")
31
+ elif "invalid" in error_msg.lower():
32
+ console.print(f"[red]❌ {error_msg}[/red]")
33
+ else:
34
+ console.print(f"[red]❌ Error: {error_msg}[/red]")
35
+ raise typer.Exit(1)
36
+ return wrapper
37
+
38
+
16
39
  def get_config_with_data():
17
40
  """Get config and load data from current directory.
18
41
 
@@ -86,6 +86,9 @@ class CinchDB:
86
86
  self.api_url = None
87
87
  self.api_key = None
88
88
  self.is_local = True
89
+
90
+ # Auto-materialize lazy database if needed
91
+ self._materialize_database_if_lazy()
89
92
  elif api_url is not None and api_key is not None:
90
93
  # Remote connection
91
94
  self.project_dir = None
@@ -111,6 +114,23 @@ class CinchDB:
111
114
  self._merge_manager: Optional["MergeManager"] = None
112
115
  self._index_manager: Optional["IndexManager"] = None
113
116
 
117
+ def _materialize_database_if_lazy(self) -> None:
118
+ """Auto-materialize a lazy database if accessing it."""
119
+ if not self.is_local:
120
+ return
121
+
122
+ # Check if this is a lazy database using metadata DB
123
+ from cinchdb.infrastructure.metadata_db import MetadataDB
124
+
125
+ with MetadataDB(self.project_dir) as metadata_db:
126
+ db_info = metadata_db.get_database(self.database)
127
+
128
+ if db_info and not db_info['materialized']:
129
+ # Database exists in metadata but not materialized
130
+ from cinchdb.core.initializer import ProjectInitializer
131
+ initializer = ProjectInitializer(self.project_dir)
132
+ initializer.materialize_database(self.database)
133
+
114
134
  @property
115
135
  def session(self):
116
136
  """Get or create HTTP session for remote connections."""
@@ -561,6 +581,122 @@ class CinchDB:
561
581
  changes.append(Change(**data))
562
582
  return changes
563
583
 
584
+ def optimize_tenant(self, tenant_name: str = None, force: bool = False) -> bool:
585
+ """Optimize a tenant's storage with VACUUM and page size adjustment.
586
+
587
+ Args:
588
+ tenant_name: Name of the tenant to optimize (default: current tenant)
589
+ force: If True, always perform optimization
590
+
591
+ Returns:
592
+ True if optimization was performed, False otherwise
593
+
594
+ Examples:
595
+ # Optimize current tenant
596
+ db.optimize_tenant()
597
+
598
+ # Optimize specific tenant
599
+ db.optimize_tenant("store_west")
600
+
601
+ # Force optimization even if not needed
602
+ db.optimize_tenant(force=True)
603
+ """
604
+ if self.is_local:
605
+ tenant_to_optimize = tenant_name or self.tenant
606
+ return self.tenants.optimize_tenant_storage(tenant_to_optimize, force=force)
607
+ else:
608
+ raise NotImplementedError("Remote tenant optimization not implemented")
609
+
610
+ def get_tenant_size(self, tenant_name: str = None) -> dict:
611
+ """Get storage size information for a tenant.
612
+
613
+ Args:
614
+ tenant_name: Name of tenant (default: current tenant)
615
+
616
+ Returns:
617
+ Dictionary with size information:
618
+ - name: Tenant name
619
+ - materialized: Whether tenant is materialized
620
+ - size_bytes: Size in bytes (0 if lazy)
621
+ - size_kb: Size in KB
622
+ - size_mb: Size in MB
623
+ - page_size: SQLite page size (if materialized)
624
+ - page_count: Number of pages (if materialized)
625
+
626
+ Examples:
627
+ # Get size of current tenant
628
+ size = db.get_tenant_size()
629
+ print(f"Current tenant uses {size['size_mb']:.2f} MB")
630
+
631
+ # Get size of specific tenant
632
+ size = db.get_tenant_size("store_west")
633
+ if size['materialized']:
634
+ print(f"Page size: {size['page_size']} bytes")
635
+ """
636
+ if self.is_local:
637
+ tenant_to_check = tenant_name or self.tenant
638
+ return self.tenants.get_tenant_size(tenant_to_check)
639
+ else:
640
+ raise NotImplementedError("Remote tenant size query not implemented")
641
+
642
+ def get_storage_info(self) -> dict:
643
+ """Get storage size information for all tenants in current branch.
644
+
645
+ Returns:
646
+ Dictionary with:
647
+ - tenants: List of individual tenant size info (sorted by size)
648
+ - total_size_bytes: Total size of all materialized tenants
649
+ - total_size_mb: Total size in MB
650
+ - lazy_count: Number of lazy tenants
651
+ - materialized_count: Number of materialized tenants
652
+
653
+ Examples:
654
+ # Get storage overview
655
+ info = db.get_storage_info()
656
+ print(f"Total storage: {info['total_size_mb']:.2f} MB")
657
+ print(f"Materialized tenants: {info['materialized_count']}")
658
+ print(f"Lazy tenants: {info['lazy_count']}")
659
+
660
+ # List largest tenants
661
+ for tenant in info['tenants'][:5]: # Top 5
662
+ if tenant['materialized']:
663
+ print(f"{tenant['name']}: {tenant['size_mb']:.2f} MB")
664
+ """
665
+ if self.is_local:
666
+ return self.tenants.get_all_tenant_sizes()
667
+ else:
668
+ raise NotImplementedError("Remote storage info not implemented")
669
+
670
+ def optimize_all_tenants(self, force: bool = False) -> dict:
671
+ """Optimize storage for all tenants in current branch.
672
+
673
+ This is designed to be called periodically to:
674
+ - Reclaim unused space with VACUUM
675
+ - Adjust page sizes as databases grow
676
+ - Keep small databases compact
677
+
678
+ Args:
679
+ force: If True, optimize all tenants regardless of size
680
+
681
+ Returns:
682
+ Dictionary with optimization results:
683
+ - optimized: List of tenant names that were optimized
684
+ - skipped: List of tenant names that were skipped
685
+ - errors: List of tuples (tenant_name, error_message)
686
+
687
+ Examples:
688
+ # Run periodic optimization
689
+ results = db.optimize_all_tenants()
690
+ print(f"Optimized {len(results['optimized'])} tenants")
691
+
692
+ # Force optimization of all tenants
693
+ results = db.optimize_all_tenants(force=True)
694
+ """
695
+ if self.is_local:
696
+ return self.tenants.optimize_all_tenants(force=force)
697
+ else:
698
+ raise NotImplementedError("Remote tenant optimization not implemented")
699
+
564
700
  def close(self):
565
701
  """Close any open connections."""
566
702
  if not self.is_local and self._session:
@@ -632,17 +768,6 @@ def connect_api(
632
768
 
633
769
  Returns:
634
770
  CinchDB connection instance for remote API
635
-
636
- Examples:
637
- # Connect to remote API
638
- db = connect_api("https://api.example.com", "your-api-key", "mydb")
639
-
640
- # Connect to specific branch
641
- db = connect_api("https://api.example.com", "your-api-key", "mydb", "dev")
642
-
643
- # Use with context manager
644
- with connect_api("https://api.example.com", "key", "mydb") as db:
645
- results = db.query("SELECT * FROM users")
646
771
  """
647
772
  return CinchDB(
648
773
  database=database,
@@ -651,3 +776,5 @@ def connect_api(
651
776
  api_url=api_url,
652
777
  api_key=api_key,
653
778
  )
779
+
780
+