cinchdb 0.1.13__py3-none-any.whl → 0.1.15__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 CHANGED
@@ -1,7 +1,20 @@
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
- __version__ = "0.1.0"
6
+ try:
7
+ from importlib.metadata import version
8
+ __version__ = version("cinchdb")
9
+ except ImportError:
10
+ # Fallback for Python < 3.8
11
+ from importlib_metadata import version
12
+ __version__ = version("cinchdb")
13
+ except Exception:
14
+ # Final fallback if package metadata is not available
15
+ __version__ = "0.1.13"
6
16
 
7
- __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)
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\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()