cinchdb 0.1.14__py3-none-any.whl → 0.1.17__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cinchdb/__init__.py +5 -1
- cinchdb/cli/commands/__init__.py +2 -1
- cinchdb/cli/commands/data.py +350 -0
- cinchdb/cli/commands/index.py +2 -2
- cinchdb/cli/commands/tenant.py +47 -0
- cinchdb/cli/main.py +3 -6
- cinchdb/config.py +4 -13
- cinchdb/core/connection.py +14 -18
- cinchdb/core/database.py +224 -75
- cinchdb/core/maintenance_utils.py +43 -0
- cinchdb/core/path_utils.py +20 -22
- cinchdb/core/tenant_activation.py +216 -0
- cinchdb/infrastructure/metadata_connection_pool.py +0 -1
- cinchdb/infrastructure/metadata_db.py +108 -1
- cinchdb/managers/branch.py +1 -1
- cinchdb/managers/change_applier.py +21 -22
- cinchdb/managers/column.py +1 -1
- cinchdb/managers/data.py +190 -14
- cinchdb/managers/index.py +1 -2
- cinchdb/managers/query.py +0 -1
- cinchdb/managers/table.py +31 -6
- cinchdb/managers/tenant.py +90 -150
- cinchdb/managers/view.py +1 -1
- cinchdb/plugins/__init__.py +16 -0
- cinchdb/plugins/base.py +80 -0
- cinchdb/plugins/decorators.py +49 -0
- cinchdb/plugins/manager.py +210 -0
- {cinchdb-0.1.14.dist-info → cinchdb-0.1.17.dist-info}/METADATA +19 -24
- {cinchdb-0.1.14.dist-info → cinchdb-0.1.17.dist-info}/RECORD +32 -28
- cinchdb/core/maintenance.py +0 -73
- cinchdb/security/__init__.py +0 -1
- cinchdb/security/encryption.py +0 -108
- {cinchdb-0.1.14.dist-info → cinchdb-0.1.17.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.14.dist-info → cinchdb-0.1.17.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.14.dist-info → cinchdb-0.1.17.dist-info}/licenses/LICENSE +0 -0
cinchdb/__init__.py
CHANGED
@@ -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"]
|
cinchdb/cli/commands/__init__.py
CHANGED
@@ -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
|
cinchdb/cli/commands/index.py
CHANGED
@@ -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
|
|
cinchdb/cli/commands/tenant.py
CHANGED
@@ -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)
|
cinchdb/cli/main.py
CHANGED
@@ -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
|
|
cinchdb/config.py
CHANGED
@@ -134,17 +134,8 @@ class Config:
|
|
134
134
|
with open(self.config_path, "w") as f:
|
135
135
|
toml.dump(config_dict, f)
|
136
136
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
initialization logic.
|
142
|
-
"""
|
143
|
-
from cinchdb.core.initializer import ProjectInitializer
|
144
|
-
|
145
|
-
initializer = ProjectInitializer(self.project_dir)
|
146
|
-
config = initializer.init_project()
|
137
|
+
@property
|
138
|
+
def base_dir(self) -> Path:
|
139
|
+
"""Get the base project directory."""
|
140
|
+
return self.project_dir
|
147
141
|
|
148
|
-
# Load the config into this instance
|
149
|
-
self._config = config
|
150
|
-
return config
|
cinchdb/core/connection.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
"""SQLite connection management for CinchDB."""
|
2
2
|
|
3
|
+
import os
|
3
4
|
import sqlite3
|
4
5
|
from pathlib import Path
|
5
|
-
from typing import Optional, Dict, List
|
6
|
+
from typing import Optional, Dict, List, Any
|
6
7
|
from contextlib import contextmanager
|
7
8
|
from datetime import datetime
|
8
9
|
|
9
|
-
from cinchdb.security.encryption import encryption
|
10
10
|
|
11
11
|
|
12
12
|
# Custom datetime adapter and converter for SQLite
|
@@ -44,23 +44,19 @@ class DatabaseConnection:
|
|
44
44
|
# Ensure directory exists
|
45
45
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
46
46
|
|
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")
|
47
|
+
# Connect with row factory for dict-like access
|
48
|
+
# detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
|
49
|
+
self._conn = sqlite3.connect(
|
50
|
+
str(self.path),
|
51
|
+
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
|
52
|
+
)
|
62
53
|
|
63
|
-
#
|
54
|
+
# Configure WAL mode and settings
|
55
|
+
self._conn.execute("PRAGMA journal_mode = WAL")
|
56
|
+
self._conn.execute("PRAGMA synchronous = NORMAL")
|
57
|
+
self._conn.execute("PRAGMA wal_autocheckpoint = 0")
|
58
|
+
|
59
|
+
# Set row factory and foreign keys
|
64
60
|
self._conn.row_factory = sqlite3.Row
|
65
61
|
self._conn.execute("PRAGMA foreign_keys = ON")
|
66
62
|
self._conn.commit()
|