cinchdb 0.1.14__tar.gz → 0.1.15__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 (65) hide show
  1. {cinchdb-0.1.14 → cinchdb-0.1.15}/PKG-INFO +15 -24
  2. {cinchdb-0.1.14 → cinchdb-0.1.15}/README.md +14 -23
  3. {cinchdb-0.1.14 → cinchdb-0.1.15}/pyproject.toml +1 -1
  4. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/__init__.py +5 -1
  5. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/__init__.py +2 -1
  6. cinchdb-0.1.15/src/cinchdb/cli/commands/data.py +350 -0
  7. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/index.py +2 -2
  8. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/tenant.py +47 -0
  9. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/main.py +3 -6
  10. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/connection.py +12 -17
  11. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/database.py +207 -70
  12. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/path_utils.py +1 -1
  13. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/infrastructure/metadata_connection_pool.py +0 -1
  14. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/infrastructure/metadata_db.py +15 -1
  15. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/branch.py +1 -1
  16. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/data.py +189 -13
  17. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/index.py +1 -2
  18. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/query.py +0 -1
  19. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/table.py +30 -5
  20. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/tenant.py +89 -149
  21. cinchdb-0.1.15/src/cinchdb/plugins/__init__.py +17 -0
  22. cinchdb-0.1.15/src/cinchdb/plugins/base.py +99 -0
  23. cinchdb-0.1.15/src/cinchdb/plugins/decorators.py +45 -0
  24. cinchdb-0.1.15/src/cinchdb/plugins/manager.py +178 -0
  25. cinchdb-0.1.14/src/cinchdb/security/__init__.py +0 -1
  26. cinchdb-0.1.14/src/cinchdb/security/encryption.py +0 -108
  27. {cinchdb-0.1.14 → cinchdb-0.1.15}/.gitignore +0 -0
  28. {cinchdb-0.1.14 → cinchdb-0.1.15}/LICENSE +0 -0
  29. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/__main__.py +0 -0
  30. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/__init__.py +0 -0
  31. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/branch.py +0 -0
  32. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/codegen.py +0 -0
  33. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/column.py +0 -0
  34. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/database.py +0 -0
  35. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/query.py +0 -0
  36. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/remote.py +0 -0
  37. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/table.py +0 -0
  38. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/view.py +0 -0
  39. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/handlers/__init__.py +0 -0
  40. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
  41. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/utils.py +0 -0
  42. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/config.py +0 -0
  43. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/__init__.py +0 -0
  44. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/initializer.py +0 -0
  45. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/maintenance.py +0 -0
  46. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/__init__.py +0 -0
  47. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_applier.py +0 -0
  48. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_comparator.py +0 -0
  49. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_tracker.py +0 -0
  50. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/codegen.py +0 -0
  51. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/column.py +0 -0
  52. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/merge_manager.py +0 -0
  53. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/view.py +0 -0
  54. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/__init__.py +0 -0
  55. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/base.py +0 -0
  56. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/branch.py +0 -0
  57. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/change.py +0 -0
  58. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/database.py +0 -0
  59. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/project.py +0 -0
  60. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/table.py +0 -0
  61. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/tenant.py +0 -0
  62. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/view.py +0 -0
  63. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/utils/__init__.py +0 -0
  64. {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/utils/name_validator.py +0 -0
  65. {cinchdb-0.1.14 → cinchdb-0.1.15}/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.14
3
+ Version: 0.1.15
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
@@ -156,24 +156,16 @@ db.update("posts", post_id, {"content": "Updated content"})
156
156
  - **Python SDK**: Core functionality for local development
157
157
  - **CLI**: Full-featured command-line interface
158
158
 
159
- ## Security & Encryption
159
+ ## Security
160
160
 
161
- Optional transparent encryption for tenant databases:
161
+ CinchDB uses standard SQLite security features:
162
162
 
163
- ```bash
164
- # Enable encryption
165
- export CINCH_ENCRYPT_DATA=true
166
- export CINCH_ENCRYPTION_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
167
-
168
- # Install encryption library
169
- pip install pysqlcipher3
170
- ```
163
+ - **WAL mode**: Better concurrency and crash recovery
164
+ - **Foreign key constraints**: Enforced data integrity
165
+ - **File permissions**: Standard OS-level access control
166
+ - **Multi-tenant isolation**: Separate database files per tenant
171
167
 
172
- - **Tenant databases**: Encrypted with ChaCha20-Poly1305 (~2-5% overhead)
173
- - **Metadata**: Unencrypted for operational simplicity
174
- - **Integration**: Transparent - no code changes needed
175
-
176
- Works without encryption libraries - gracefully falls back to standard SQLite.
168
+ For production deployments, consider additional security measures at the infrastructure level.
177
169
 
178
170
  ## Development
179
171
 
@@ -186,14 +178,13 @@ make test
186
178
 
187
179
  ## Future
188
180
 
189
- Though probably not, perhaps I'll evolve it into something bigger and more full-featured, with things like
190
- - data backups
191
- - replication to S3
192
- - audit access
193
- - SaaS-like dynamics
194
- - multi-project hosting
195
- - auth proxying
196
- - leader-follower abilities for edge deployment
181
+ CinchDB focuses on being a simple, reliable SQLite management layer. Future development will prioritize:
182
+
183
+ - Remote API server improvements
184
+ - Better CLI user experience
185
+ - Performance optimizations
186
+ - Additional language SDKs (TypeScript, Go, etc.)
187
+ - Enhanced codegen features
197
188
 
198
189
 
199
190
  ## License
@@ -127,24 +127,16 @@ db.update("posts", post_id, {"content": "Updated content"})
127
127
  - **Python SDK**: Core functionality for local development
128
128
  - **CLI**: Full-featured command-line interface
129
129
 
130
- ## Security & Encryption
130
+ ## Security
131
131
 
132
- Optional transparent encryption for tenant databases:
132
+ CinchDB uses standard SQLite security features:
133
133
 
134
- ```bash
135
- # Enable encryption
136
- export CINCH_ENCRYPT_DATA=true
137
- export CINCH_ENCRYPTION_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
138
-
139
- # Install encryption library
140
- pip install pysqlcipher3
141
- ```
134
+ - **WAL mode**: Better concurrency and crash recovery
135
+ - **Foreign key constraints**: Enforced data integrity
136
+ - **File permissions**: Standard OS-level access control
137
+ - **Multi-tenant isolation**: Separate database files per tenant
142
138
 
143
- - **Tenant databases**: Encrypted with ChaCha20-Poly1305 (~2-5% overhead)
144
- - **Metadata**: Unencrypted for operational simplicity
145
- - **Integration**: Transparent - no code changes needed
146
-
147
- Works without encryption libraries - gracefully falls back to standard SQLite.
139
+ For production deployments, consider additional security measures at the infrastructure level.
148
140
 
149
141
  ## Development
150
142
 
@@ -157,14 +149,13 @@ make test
157
149
 
158
150
  ## Future
159
151
 
160
- Though probably not, perhaps I'll evolve it into something bigger and more full-featured, with things like
161
- - data backups
162
- - replication to S3
163
- - audit access
164
- - SaaS-like dynamics
165
- - multi-project hosting
166
- - auth proxying
167
- - leader-follower abilities for edge deployment
152
+ CinchDB focuses on being a simple, reliable SQLite management layer. Future development will prioritize:
153
+
154
+ - Remote API server improvements
155
+ - Better CLI user experience
156
+ - Performance optimizations
157
+ - Additional language SDKs (TypeScript, Go, etc.)
158
+ - Enhanced codegen features
168
159
 
169
160
 
170
161
  ## License
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinchdb"
3
- version = "0.1.14"
3
+ version = "0.1.15"
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"
@@ -1,6 +1,7 @@
1
1
  """CinchDB - A Git-like SQLite database management system."""
2
2
 
3
3
  from cinchdb.core.database import connect, connect_api
4
+ from cinchdb.plugins.manager import PluginManager
4
5
 
5
6
  try:
6
7
  from importlib.metadata import version
@@ -13,4 +14,7 @@ except Exception:
13
14
  # Final fallback if package metadata is not available
14
15
  __version__ = "0.1.13"
15
16
 
16
- __all__ = ["connect", "connect_api"]
17
+ # Global plugin manager
18
+ plugin_manager = PluginManager()
19
+
20
+ __all__ = ["connect", "connect_api", "plugin_manager"]
@@ -1,6 +1,6 @@
1
1
  """CLI command modules."""
2
2
 
3
- from . import database, branch, tenant, table, column, view, query, codegen, remote, index
3
+ from . import database, branch, tenant, table, column, view, query, codegen, remote, index, data
4
4
 
5
5
  __all__ = [
6
6
  "database",
@@ -13,4 +13,5 @@ __all__ = [
13
13
  "codegen",
14
14
  "remote",
15
15
  "index",
16
+ "data",
16
17
  ]
@@ -0,0 +1,350 @@
1
+ """Data manipulation commands for CinchDB CLI."""
2
+
3
+ import typer
4
+ from typing import Optional
5
+ from rich.console import Console
6
+
7
+ from cinchdb.cli.utils import get_config_with_data
8
+
9
+ app = typer.Typer(help="Data manipulation commands", invoke_without_command=True)
10
+ console = Console()
11
+
12
+
13
+ @app.callback()
14
+ def callback(ctx: typer.Context):
15
+ """Show help when no subcommand is provided."""
16
+ if ctx.invoked_subcommand is None:
17
+ console.print(ctx.get_help())
18
+ raise typer.Exit(0)
19
+
20
+
21
+ @app.command()
22
+ def delete(
23
+ table_name: str = typer.Argument(..., help="Name of table to delete from"),
24
+ where: str = typer.Option(..., "--where", "-w", help="Filter conditions (e.g., 'status=inactive' or 'age__gt=65' or 'id__in=1,2,3')"),
25
+ tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
26
+ confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip confirmation prompt"),
27
+ ):
28
+ """Delete records from a table based on filter criteria.
29
+
30
+ Examples:
31
+ cinch data delete users --where "status=inactive"
32
+ cinch data delete items --where "price__gt=100"
33
+ cinch data delete logs --where "id__in=1,2,3,4,5"
34
+ """
35
+ from cinchdb.core.database import CinchDB
36
+
37
+ config, config_data = get_config_with_data()
38
+
39
+ # Parse conditions into filter dict
40
+ try:
41
+ filters = _parse_conditions(where)
42
+ except ValueError as e:
43
+ console.print(f"[red]❌ Invalid conditions format: {e}[/red]")
44
+ raise typer.Exit(1)
45
+
46
+ if not confirm:
47
+ console.print(f"[yellow]⚠️ About to delete records from table '{table_name}' where: {where}[/yellow]")
48
+ console.print(f"[yellow] Tenant: {tenant}[/yellow]")
49
+ confirm_delete = typer.confirm("Are you sure you want to proceed?")
50
+ if not confirm_delete:
51
+ console.print("Operation cancelled.")
52
+ raise typer.Exit(0)
53
+
54
+ try:
55
+ db = CinchDB(config_data.active_database, tenant=tenant, project_dir=config.project_dir)
56
+
57
+ console.print(f"[yellow]🗑️ Deleting records from '{table_name}'...[/yellow]")
58
+
59
+ deleted_count = db.delete_where(table_name, **filters)
60
+
61
+ if deleted_count > 0:
62
+ console.print(f"[green]✅ Deleted {deleted_count} record(s) from '{table_name}'[/green]")
63
+ else:
64
+ console.print(f"[blue]ℹ️ No records matched the criteria in '{table_name}'[/blue]")
65
+
66
+ except ValueError as e:
67
+ console.print(f"[red]❌ {e}[/red]")
68
+ raise typer.Exit(1)
69
+
70
+
71
+ @app.command()
72
+ def update(
73
+ table_name: str = typer.Argument(..., help="Name of table to update"),
74
+ set_data: str = typer.Option(..., "--set", "-s", help="Data to update (e.g., 'status=active,priority=high')"),
75
+ where: str = typer.Option(..., "--where", "-w", help="Filter conditions (e.g., 'status=inactive' or 'age__gt=65')"),
76
+ tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
77
+ confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip confirmation prompt"),
78
+ ):
79
+ """Update records in a table based on filter criteria.
80
+
81
+ Examples:
82
+ cinch data update users --set "status=active" --where "status=inactive"
83
+ cinch data update items --set "price=99.99,category=sale" --where "price__gt=100"
84
+ """
85
+ from cinchdb.core.database import CinchDB
86
+
87
+ config, config_data = get_config_with_data()
88
+
89
+ # Parse conditions and data
90
+ try:
91
+ filters = _parse_conditions(where)
92
+ update_data = _parse_set_data(set_data)
93
+ except ValueError as e:
94
+ console.print(f"[red]❌ Invalid format: {e}[/red]")
95
+ raise typer.Exit(1)
96
+
97
+ if not confirm:
98
+ console.print(f"[yellow]⚠️ About to update records in table '{table_name}'[/yellow]")
99
+ console.print(f"[yellow] Set: {set_data}[/yellow]")
100
+ console.print(f"[yellow] Where: {where}[/yellow]")
101
+ console.print(f"[yellow] Tenant: {tenant}[/yellow]")
102
+ confirm_update = typer.confirm("Are you sure you want to proceed?")
103
+ if not confirm_update:
104
+ console.print("Operation cancelled.")
105
+ raise typer.Exit(0)
106
+
107
+ try:
108
+ db = CinchDB(config_data.active_database, tenant=tenant, project_dir=config.project_dir)
109
+
110
+ console.print(f"[yellow]📝 Updating records in '{table_name}'...[/yellow]")
111
+
112
+ updated_count = db.update_where(table_name, update_data, **filters)
113
+
114
+ if updated_count > 0:
115
+ console.print(f"[green]✅ Updated {updated_count} record(s) in '{table_name}'[/green]")
116
+ else:
117
+ console.print(f"[blue]ℹ️ No records matched the criteria in '{table_name}'[/blue]")
118
+
119
+ except ValueError as e:
120
+ console.print(f"[red]❌ {e}[/red]")
121
+ raise typer.Exit(1)
122
+
123
+
124
+ @app.command()
125
+ def bulk_update(
126
+ table_name: str = typer.Argument(..., help="Name of table to update"),
127
+ data: str = typer.Option(..., "--data", "-d", help="JSON array of update objects with 'id' field"),
128
+ tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
129
+ confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip confirmation prompt"),
130
+ ):
131
+ """Update multiple records in a table using JSON data.
132
+
133
+ Examples:
134
+ cinch data bulk-update users --data '[{"id":"123","name":"John"},{"id":"456","status":"active"}]'
135
+ cinch data bulk-update items --data '[{"id":"1","price":99.99},{"id":"2","category":"sale"}]'
136
+ """
137
+ import json
138
+ from cinchdb.core.database import CinchDB
139
+
140
+ config, config_data = get_config_with_data()
141
+
142
+ # Parse JSON data
143
+ try:
144
+ updates = json.loads(data)
145
+ if not isinstance(updates, list):
146
+ updates = [updates]
147
+ except json.JSONDecodeError as e:
148
+ console.print(f"[red]❌ Invalid JSON format: {e}[/red]")
149
+ raise typer.Exit(1)
150
+
151
+ if not confirm:
152
+ console.print(f"[yellow]⚠️ About to update {len(updates)} record(s) in table '{table_name}'[/yellow]")
153
+ console.print(f"[yellow] Tenant: {tenant}[/yellow]")
154
+ console.print(f"[yellow] Data preview: {json.dumps(updates[:3], indent=2)}{'...' if len(updates) > 3 else ''}[/yellow]")
155
+ confirm_update = typer.confirm("Are you sure you want to proceed?")
156
+ if not confirm_update:
157
+ console.print("Operation cancelled.")
158
+ raise typer.Exit(0)
159
+
160
+ try:
161
+ db = CinchDB(config_data.active_database, tenant=tenant, project_dir=config.project_dir)
162
+
163
+ console.print(f"[yellow]📝 Updating {len(updates)} record(s) in '{table_name}'...[/yellow]")
164
+
165
+ result = db.update(table_name, *updates)
166
+ updated_count = len(updates)
167
+
168
+ console.print(f"[green]✅ Updated {updated_count} record(s) in '{table_name}'[/green]")
169
+
170
+ except ValueError as e:
171
+ console.print(f"[red]❌ {e}[/red]")
172
+ raise typer.Exit(1)
173
+
174
+
175
+ @app.command()
176
+ def bulk_delete(
177
+ table_name: str = typer.Argument(..., help="Name of table to delete from"),
178
+ ids: str = typer.Option(..., "--ids", "-i", help="Comma-separated list of IDs or JSON array"),
179
+ tenant: Optional[str] = typer.Option("main", "--tenant", "-t", help="Tenant name"),
180
+ confirm: bool = typer.Option(False, "--confirm", "-y", help="Skip confirmation prompt"),
181
+ ):
182
+ """Delete multiple records from a table by IDs.
183
+
184
+ Examples:
185
+ cinch data bulk-delete users --ids "123,456,789"
186
+ cinch data bulk-delete items --ids '["abc","def","ghi"]'
187
+ """
188
+ import json
189
+ from cinchdb.core.database import CinchDB
190
+
191
+ config, config_data = get_config_with_data()
192
+
193
+ # Parse IDs - try JSON first, then comma-separated
194
+ try:
195
+ id_list = json.loads(ids)
196
+ if not isinstance(id_list, list):
197
+ id_list = [str(id_list)]
198
+ except json.JSONDecodeError:
199
+ # Try comma-separated format
200
+ id_list = [id_str.strip() for id_str in ids.split(",") if id_str.strip()]
201
+
202
+ if not id_list:
203
+ console.print("[red]❌ No valid IDs provided[/red]")
204
+ raise typer.Exit(1)
205
+
206
+ if not confirm:
207
+ console.print(f"[yellow]⚠️ About to delete {len(id_list)} record(s) from table '{table_name}'[/yellow]")
208
+ console.print(f"[yellow] Tenant: {tenant}[/yellow]")
209
+ console.print(f"[yellow] IDs: {id_list[:5]}{'...' if len(id_list) > 5 else ''}[/yellow]")
210
+ confirm_delete = typer.confirm("Are you sure you want to proceed?")
211
+ if not confirm_delete:
212
+ console.print("Operation cancelled.")
213
+ raise typer.Exit(0)
214
+
215
+ try:
216
+ db = CinchDB(config_data.active_database, tenant=tenant, project_dir=config.project_dir)
217
+
218
+ console.print(f"[yellow]🗑️ Deleting {len(id_list)} record(s) from '{table_name}'...[/yellow]")
219
+
220
+ deleted_count = db.delete(table_name, *id_list)
221
+
222
+ console.print(f"[green]✅ Deleted {deleted_count} record(s) from '{table_name}'[/green]")
223
+
224
+ except ValueError as e:
225
+ console.print(f"[red]❌ {e}[/red]")
226
+ raise typer.Exit(1)
227
+
228
+
229
+ def _parse_conditions(conditions_str: str) -> dict:
230
+ """Parse condition string into filter dictionary."""
231
+ filters = {}
232
+
233
+ # Split by commas but handle quotes
234
+ parts = []
235
+ current_part = ""
236
+ in_quotes = False
237
+
238
+ for char in conditions_str:
239
+ if char == '"' and not in_quotes:
240
+ in_quotes = True
241
+ current_part += char
242
+ elif char == '"' and in_quotes:
243
+ in_quotes = False
244
+ current_part += char
245
+ elif char == ',' and not in_quotes:
246
+ if current_part.strip():
247
+ parts.append(current_part.strip())
248
+ current_part = ""
249
+ else:
250
+ current_part += char
251
+
252
+ if current_part.strip():
253
+ parts.append(current_part.strip())
254
+
255
+ for part in parts:
256
+ if '=' not in part:
257
+ raise ValueError(f"Invalid condition format: '{part}'. Expected format: 'column=value' or 'column__operator=value'")
258
+
259
+ key, value_str = part.split('=', 1)
260
+ key = key.strip()
261
+ value_str = value_str.strip()
262
+
263
+ # Remove quotes if present
264
+ if value_str.startswith('"') and value_str.endswith('"'):
265
+ value_str = value_str[1:-1]
266
+
267
+ # Handle special cases
268
+ if '__in' in key:
269
+ # Convert comma-separated values to list
270
+ value = [v.strip() for v in value_str.split(',')]
271
+ # Try to convert to numbers if possible
272
+ try:
273
+ value = [int(v) for v in value]
274
+ except ValueError:
275
+ try:
276
+ value = [float(v) for v in value]
277
+ except ValueError:
278
+ pass # Keep as strings
279
+ else:
280
+ # Try to convert to appropriate type
281
+ value = _convert_value(value_str)
282
+
283
+ filters[key] = value
284
+
285
+ return filters
286
+
287
+
288
+ def _parse_set_data(set_str: str) -> dict:
289
+ """Parse set string into update data dictionary."""
290
+ data = {}
291
+
292
+ # Split by commas but handle quotes (similar to conditions)
293
+ parts = []
294
+ current_part = ""
295
+ in_quotes = False
296
+
297
+ for char in set_str:
298
+ if char == '"' and not in_quotes:
299
+ in_quotes = True
300
+ current_part += char
301
+ elif char == '"' and in_quotes:
302
+ in_quotes = False
303
+ current_part += char
304
+ elif char == ',' and not in_quotes:
305
+ if current_part.strip():
306
+ parts.append(current_part.strip())
307
+ current_part = ""
308
+ else:
309
+ current_part += char
310
+
311
+ if current_part.strip():
312
+ parts.append(current_part.strip())
313
+
314
+ for part in parts:
315
+ if '=' not in part:
316
+ raise ValueError(f"Invalid set format: '{part}'. Expected format: 'column=value'")
317
+
318
+ key, value_str = part.split('=', 1)
319
+ key = key.strip()
320
+ value_str = value_str.strip()
321
+
322
+ # Remove quotes if present
323
+ if value_str.startswith('"') and value_str.endswith('"'):
324
+ value_str = value_str[1:-1]
325
+
326
+ data[key] = _convert_value(value_str)
327
+
328
+ return data
329
+
330
+
331
+ def _convert_value(value_str: str):
332
+ """Convert string value to appropriate Python type."""
333
+ # Try integer
334
+ try:
335
+ return int(value_str)
336
+ except ValueError:
337
+ pass
338
+
339
+ # Try float
340
+ try:
341
+ return float(value_str)
342
+ except ValueError:
343
+ pass
344
+
345
+ # Try boolean
346
+ if value_str.lower() in ('true', 'false'):
347
+ return value_str.lower() == 'true'
348
+
349
+ # Default to string
350
+ return value_str
@@ -169,11 +169,11 @@ def index_info(
169
169
  print(f"Partial: [blue]{'Yes' if info.get('partial') else 'No'}[/blue]")
170
170
 
171
171
  if info.get('sql'):
172
- print(f"\nSQL Definition:")
172
+ print("\nSQL Definition:")
173
173
  print(f"[dim]{info['sql']}[/dim]")
174
174
 
175
175
  if info.get('columns_info'):
176
- print(f"\nColumn Details:")
176
+ print("\nColumn Details:")
177
177
  for col in info['columns_info']:
178
178
  print(f" - Position {col['position']}: {col['column_name']}")
179
179
 
@@ -188,3 +188,50 @@ def rename(
188
188
  except ValueError as e:
189
189
  console.print(f"[red]❌ {e}[/red]")
190
190
  raise typer.Exit(1)
191
+
192
+
193
+ @app.command()
194
+ def vacuum(
195
+ tenant_name: str = typer.Argument(..., help="Name of tenant to vacuum"),
196
+ ):
197
+ """Run VACUUM operation on a tenant to optimize storage and performance.
198
+
199
+ VACUUM reclaims space from deleted records, defragments the database file,
200
+ and can improve query performance. This is especially useful after
201
+ deleting large amounts of data.
202
+
203
+ Examples:
204
+ cinch tenant vacuum main
205
+ cinch tenant vacuum store_east
206
+ """
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
+ tenant_mgr = TenantManager(config.project_dir, db_name, branch_name)
213
+
214
+ console.print(f"[yellow]🔧 Starting VACUUM operation on tenant '{tenant_name}'...[/yellow]")
215
+
216
+ result = tenant_mgr.vacuum_tenant(tenant_name)
217
+
218
+ if result['success']:
219
+ console.print("[green]✅ VACUUM completed successfully[/green]")
220
+ console.print(f" Tenant: {result['tenant']}")
221
+ console.print(f" Size before: {result['size_before']:,} bytes ({result['size_before'] / (1024*1024):.2f} MB)")
222
+ console.print(f" Size after: {result['size_after']:,} bytes ({result['size_after'] / (1024*1024):.2f} MB)")
223
+ console.print(f" Space reclaimed: {result['space_reclaimed']:,} bytes ({result['space_reclaimed_mb']} MB)")
224
+ console.print(f" Duration: {result['duration_seconds']} seconds")
225
+
226
+ if result['space_reclaimed'] > 0:
227
+ percent_saved = (result['space_reclaimed'] / result['size_before']) * 100
228
+ console.print(f" [green]💾 Saved {percent_saved:.1f}% of space[/green]")
229
+ else:
230
+ console.print(" [blue]ℹ️ No space to reclaim (database was already optimized)[/blue]")
231
+ else:
232
+ console.print(f"[red]❌ VACUUM failed: {result.get('error', 'Unknown error')}[/red]")
233
+ raise typer.Exit(1)
234
+
235
+ except ValueError as e:
236
+ console.print(f"[red]❌ {e}[/red]")
237
+ raise typer.Exit(1)
@@ -15,10 +15,11 @@ from cinchdb.cli.commands import (
15
15
  codegen,
16
16
  index,
17
17
  )
18
+ from cinchdb.cli.commands.data import app as data_app
18
19
 
19
20
  app = typer.Typer(
20
21
  name="cinch",
21
- help="CinchDB - A Git-like SQLite database management system\n\nENCRYPTION: Set CINCH_ENCRYPT_DATA=true to enable tenant database encryption.",
22
+ help="CinchDB - A Git-like SQLite database management system",
22
23
  add_completion=False,
23
24
  invoke_without_command=True,
24
25
  )
@@ -28,11 +29,6 @@ app = typer.Typer(
28
29
  def main(ctx: typer.Context):
29
30
  """
30
31
  CinchDB - A Git-like SQLite database management system
31
-
32
- ENCRYPTION:
33
- Set CINCH_ENCRYPT_DATA=true to enable tenant database encryption.
34
- Set CINCH_ENCRYPTION_KEY=your-key to provide encryption key.
35
- Requires SQLite3MultipleCiphers for encryption support.
36
32
  """
37
33
  if ctx.invoked_subcommand is None:
38
34
  # No subcommand was invoked, show help
@@ -48,6 +44,7 @@ app.add_typer(table.app, name="table", help="Table management commands")
48
44
  app.add_typer(column.app, name="column", help="Column management commands")
49
45
  app.add_typer(view.app, name="view", help="View management commands")
50
46
  app.add_typer(index.app, name="index", help="Index management commands")
47
+ app.add_typer(data_app, name="data", help="Data manipulation commands")
51
48
  app.add_typer(codegen.app, name="codegen", help="Code generation commands")
52
49
 
53
50
 
@@ -6,7 +6,6 @@ from typing import Optional, Dict, List
6
6
  from contextlib import contextmanager
7
7
  from datetime import datetime
8
8
 
9
- from cinchdb.security.encryption import encryption
10
9
 
11
10
 
12
11
  # Custom datetime adapter and converter for SQLite
@@ -44,23 +43,19 @@ class DatabaseConnection:
44
43
  # Ensure directory exists
45
44
  self.path.parent.mkdir(parents=True, exist_ok=True)
46
45
 
47
- # Use encryption if enabled, otherwise standard connection
48
- if encryption.enabled:
49
- self._conn = encryption.get_connection(self.path)
50
- else:
51
- # Connect with row factory for dict-like access
52
- # detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
53
- self._conn = sqlite3.connect(
54
- str(self.path),
55
- detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
56
- )
57
-
58
- # Configure WAL mode and settings (encryption module handles this if enabled)
59
- self._conn.execute("PRAGMA journal_mode = WAL")
60
- self._conn.execute("PRAGMA synchronous = NORMAL")
61
- self._conn.execute("PRAGMA wal_autocheckpoint = 0")
46
+ # Connect with row factory for dict-like access
47
+ # detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
48
+ self._conn = sqlite3.connect(
49
+ str(self.path),
50
+ detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
51
+ )
62
52
 
63
- # Always set row factory and foreign keys
53
+ # Configure WAL mode and settings
54
+ self._conn.execute("PRAGMA journal_mode = WAL")
55
+ self._conn.execute("PRAGMA synchronous = NORMAL")
56
+ self._conn.execute("PRAGMA wal_autocheckpoint = 0")
57
+
58
+ # Set row factory and foreign keys
64
59
  self._conn.row_factory = sqlite3.Row
65
60
  self._conn.execute("PRAGMA foreign_keys = ON")
66
61
  self._conn.commit()