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.
- {cinchdb-0.1.14 → cinchdb-0.1.15}/PKG-INFO +15 -24
- {cinchdb-0.1.14 → cinchdb-0.1.15}/README.md +14 -23
- {cinchdb-0.1.14 → cinchdb-0.1.15}/pyproject.toml +1 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/__init__.py +5 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/__init__.py +2 -1
- cinchdb-0.1.15/src/cinchdb/cli/commands/data.py +350 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/index.py +2 -2
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/tenant.py +47 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/main.py +3 -6
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/connection.py +12 -17
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/database.py +207 -70
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/path_utils.py +1 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/infrastructure/metadata_connection_pool.py +0 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/infrastructure/metadata_db.py +15 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/branch.py +1 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/data.py +189 -13
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/index.py +1 -2
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/query.py +0 -1
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/table.py +30 -5
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/tenant.py +89 -149
- cinchdb-0.1.15/src/cinchdb/plugins/__init__.py +17 -0
- cinchdb-0.1.15/src/cinchdb/plugins/base.py +99 -0
- cinchdb-0.1.15/src/cinchdb/plugins/decorators.py +45 -0
- cinchdb-0.1.15/src/cinchdb/plugins/manager.py +178 -0
- cinchdb-0.1.14/src/cinchdb/security/__init__.py +0 -1
- cinchdb-0.1.14/src/cinchdb/security/encryption.py +0 -108
- {cinchdb-0.1.14 → cinchdb-0.1.15}/.gitignore +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/LICENSE +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/__main__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/branch.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/codegen.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/column.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/database.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/query.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/remote.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/table.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/commands/view.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/handlers/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/cli/utils.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/config.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/initializer.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/core/maintenance.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_applier.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_comparator.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/change_tracker.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/codegen.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/column.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/merge_manager.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/managers/view.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/base.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/branch.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/change.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/database.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/project.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/table.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/tenant.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/models/view.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/utils/__init__.py +0 -0
- {cinchdb-0.1.14 → cinchdb-0.1.15}/src/cinchdb/utils/name_validator.py +0 -0
- {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.
|
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
|
159
|
+
## Security
|
160
160
|
|
161
|
-
|
161
|
+
CinchDB uses standard SQLite security features:
|
162
162
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
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
|
-
|
190
|
-
|
191
|
-
-
|
192
|
-
-
|
193
|
-
-
|
194
|
-
-
|
195
|
-
-
|
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
|
130
|
+
## Security
|
131
131
|
|
132
|
-
|
132
|
+
CinchDB uses standard SQLite security features:
|
133
133
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
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
|
-
|
161
|
-
|
162
|
-
-
|
163
|
-
-
|
164
|
-
-
|
165
|
-
-
|
166
|
-
-
|
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,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
|
-
|
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(
|
172
|
+
print("\nSQL Definition:")
|
173
173
|
print(f"[dim]{info['sql']}[/dim]")
|
174
174
|
|
175
175
|
if info.get('columns_info'):
|
176
|
-
print(
|
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
|
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
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
-
#
|
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()
|