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 +15 -2
- 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/core/connection.py +12 -17
- cinchdb/core/database.py +207 -70
- cinchdb/core/path_utils.py +1 -1
- cinchdb/infrastructure/metadata_connection_pool.py +0 -1
- cinchdb/infrastructure/metadata_db.py +15 -1
- cinchdb/managers/branch.py +1 -1
- cinchdb/managers/data.py +189 -13
- cinchdb/managers/index.py +1 -2
- cinchdb/managers/query.py +0 -1
- cinchdb/managers/table.py +30 -5
- cinchdb/managers/tenant.py +89 -149
- cinchdb/plugins/__init__.py +17 -0
- cinchdb/plugins/base.py +99 -0
- cinchdb/plugins/decorators.py +45 -0
- cinchdb/plugins/manager.py +178 -0
- {cinchdb-0.1.13.dist-info → cinchdb-0.1.15.dist-info}/METADATA +15 -24
- {cinchdb-0.1.13.dist-info → cinchdb-0.1.15.dist-info}/RECORD +26 -23
- cinchdb/security/__init__.py +0 -1
- cinchdb/security/encryption.py +0 -108
- {cinchdb-0.1.13.dist-info → cinchdb-0.1.15.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.13.dist-info → cinchdb-0.1.15.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.13.dist-info → cinchdb-0.1.15.dist-info}/licenses/LICENSE +0 -0
cinchdb/core/database.py
CHANGED
@@ -464,38 +464,189 @@ class CinchDB:
|
|
464
464
|
)
|
465
465
|
return result
|
466
466
|
|
467
|
-
def update(self, table: str,
|
468
|
-
"""Update
|
467
|
+
def update(self, table: str, *updates: Dict[str, Any]) -> Dict[str, Any] | List[Dict[str, Any]]:
|
468
|
+
"""Update one or more records in a table.
|
469
469
|
|
470
470
|
Args:
|
471
471
|
table: Table name
|
472
|
-
|
473
|
-
data: Updated data as dictionary
|
472
|
+
*updates: One or more update dictionaries, each must contain 'id' field
|
474
473
|
|
475
474
|
Returns:
|
476
|
-
|
475
|
+
Single record dict if one record updated, list of dicts if multiple
|
476
|
+
|
477
|
+
Examples:
|
478
|
+
# Single update
|
479
|
+
db.update("users", {"id": "123", "name": "John Updated", "status": "active"})
|
480
|
+
|
481
|
+
# Multiple updates using star expansion
|
482
|
+
db.update("users",
|
483
|
+
{"id": "123", "name": "John Updated", "status": "active"},
|
484
|
+
{"id": "456", "name": "Jane Updated", "email": "jane.new@example.com"},
|
485
|
+
{"id": "789", "status": "inactive"}
|
486
|
+
)
|
487
|
+
|
488
|
+
# Or with a list using star expansion
|
489
|
+
user_updates = [
|
490
|
+
{"id": "abc", "name": "Alice Updated"},
|
491
|
+
{"id": "def", "status": "premium"}
|
492
|
+
]
|
493
|
+
db.update("users", *user_updates)
|
477
494
|
"""
|
495
|
+
if not updates:
|
496
|
+
raise ValueError("At least one update record must be provided")
|
497
|
+
|
498
|
+
# Validate that all updates have an 'id' field
|
499
|
+
for i, update_data in enumerate(updates):
|
500
|
+
if 'id' not in update_data:
|
501
|
+
raise ValueError(f"Update record {i} missing required 'id' field")
|
502
|
+
|
478
503
|
if self.is_local:
|
479
|
-
|
504
|
+
# Single record
|
505
|
+
if len(updates) == 1:
|
506
|
+
update_data = updates[0].copy()
|
507
|
+
record_id = update_data.pop('id')
|
508
|
+
return self.data.update_by_id(table, record_id, update_data)
|
509
|
+
|
510
|
+
# Multiple records - batch update
|
511
|
+
results = []
|
512
|
+
for update_data in updates:
|
513
|
+
update_copy = update_data.copy()
|
514
|
+
record_id = update_copy.pop('id')
|
515
|
+
result = self.data.update_by_id(table, record_id, update_copy)
|
516
|
+
results.append(result)
|
517
|
+
return results
|
480
518
|
else:
|
481
|
-
# Remote update
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
519
|
+
# Remote update
|
520
|
+
if len(updates) == 1:
|
521
|
+
# Single record - use existing endpoint
|
522
|
+
update_data = updates[0].copy()
|
523
|
+
record_id = update_data.pop('id')
|
524
|
+
result = self._make_request(
|
525
|
+
"PUT", f"/tables/{table}/data/{record_id}", json={"data": update_data}
|
526
|
+
)
|
527
|
+
return result
|
528
|
+
else:
|
529
|
+
# Multiple records - use bulk endpoint
|
530
|
+
result = self._make_request(
|
531
|
+
"PUT", f"/tables/{table}/data/bulk", json={"updates": list(updates)}
|
532
|
+
)
|
533
|
+
return result
|
486
534
|
|
487
|
-
def delete(self, table: str,
|
488
|
-
"""Delete
|
535
|
+
def delete(self, table: str, *ids: str) -> int:
|
536
|
+
"""Delete one or more records from a table.
|
489
537
|
|
490
538
|
Args:
|
491
539
|
table: Table name
|
492
|
-
|
540
|
+
*ids: One or more record IDs
|
541
|
+
|
542
|
+
Returns:
|
543
|
+
Number of records deleted
|
544
|
+
|
545
|
+
Examples:
|
546
|
+
# Single delete
|
547
|
+
db.delete("users", "123")
|
548
|
+
|
549
|
+
# Multiple deletes
|
550
|
+
db.delete("users", "123", "456", "789")
|
551
|
+
|
552
|
+
# Or with a list using star expansion
|
553
|
+
user_ids = ["abc", "def", "ghi"]
|
554
|
+
db.delete("users", *user_ids)
|
493
555
|
"""
|
556
|
+
if not ids:
|
557
|
+
raise ValueError("At least one ID must be provided")
|
558
|
+
|
494
559
|
if self.is_local:
|
495
|
-
|
560
|
+
# Single record
|
561
|
+
if len(ids) == 1:
|
562
|
+
success = self.data.delete_by_id(table, ids[0])
|
563
|
+
return 1 if success else 0
|
564
|
+
|
565
|
+
# Multiple records - batch delete
|
566
|
+
deleted_count = 0
|
567
|
+
for record_id in ids:
|
568
|
+
success = self.data.delete_by_id(table, record_id)
|
569
|
+
if success:
|
570
|
+
deleted_count += 1
|
571
|
+
return deleted_count
|
496
572
|
else:
|
497
573
|
# Remote delete
|
498
|
-
|
574
|
+
if len(ids) == 1:
|
575
|
+
# Single record - use existing endpoint
|
576
|
+
self._make_request("DELETE", f"/tables/{table}/data/{ids[0]}")
|
577
|
+
return 1
|
578
|
+
else:
|
579
|
+
# Multiple records - use bulk endpoint
|
580
|
+
result = self._make_request(
|
581
|
+
"DELETE", f"/tables/{table}/data/bulk", json={"ids": list(ids)}
|
582
|
+
)
|
583
|
+
return result.get("deleted_count", len(ids))
|
584
|
+
|
585
|
+
def delete_where(self, table: str, operator: str = "AND", **filters) -> int:
|
586
|
+
"""Delete records from a table based on filter criteria.
|
587
|
+
|
588
|
+
Args:
|
589
|
+
table: Table name
|
590
|
+
operator: Logical operator to combine conditions - "AND" (default) or "OR"
|
591
|
+
**filters: Filter criteria (supports operators like __gt, __lt, __in, __like, __not)
|
592
|
+
Multiple conditions are combined with the specified operator
|
593
|
+
|
594
|
+
Returns:
|
595
|
+
Number of records deleted
|
596
|
+
|
597
|
+
Examples:
|
598
|
+
# Delete records where status = 'inactive' (single condition)
|
599
|
+
count = db.delete_where('users', status='inactive')
|
600
|
+
|
601
|
+
# Delete records where status = 'inactive' AND age > 65 (default AND)
|
602
|
+
count = db.delete_where('users', status='inactive', age__gt=65)
|
603
|
+
|
604
|
+
# Delete records where status = 'inactive' OR age > 65
|
605
|
+
count = db.delete_where('users', operator='OR', status='inactive', age__gt=65)
|
606
|
+
|
607
|
+
# Delete records where item_id in [1, 2, 3]
|
608
|
+
count = db.delete_where('items', item_id__in=[1, 2, 3])
|
609
|
+
"""
|
610
|
+
if self.is_local:
|
611
|
+
return self.data.delete_where(table, operator=operator, **filters)
|
612
|
+
else:
|
613
|
+
raise NotImplementedError("Remote bulk delete not implemented")
|
614
|
+
|
615
|
+
def update_where(self, table: str, data: Dict[str, Any], operator: str = "AND", **filters) -> int:
|
616
|
+
"""Update records in a table based on filter criteria.
|
617
|
+
|
618
|
+
Args:
|
619
|
+
table: Table name
|
620
|
+
data: Dictionary of column-value pairs to update
|
621
|
+
operator: Logical operator to combine conditions - "AND" (default) or "OR"
|
622
|
+
**filters: Filter criteria (supports operators like __gt, __lt, __in, __like, __not)
|
623
|
+
Multiple conditions are combined with the specified operator
|
624
|
+
|
625
|
+
Returns:
|
626
|
+
Number of records updated
|
627
|
+
|
628
|
+
Examples:
|
629
|
+
# Update status for all users with age > 65 (single condition)
|
630
|
+
count = db.update_where('users', {'status': 'senior'}, age__gt=65)
|
631
|
+
|
632
|
+
# Update status where age > 65 AND status = 'active' (default AND)
|
633
|
+
count = db.update_where('users', {'status': 'senior'}, age__gt=65, status='active')
|
634
|
+
|
635
|
+
# Update status where age > 65 OR status = 'pending'
|
636
|
+
count = db.update_where('users', {'status': 'senior'}, operator='OR', age__gt=65, status='pending')
|
637
|
+
|
638
|
+
# Update multiple fields where item_id in specific list
|
639
|
+
count = db.update_where(
|
640
|
+
'items',
|
641
|
+
{'status': 'inactive', 'updated_at': datetime.now()},
|
642
|
+
item_id__in=[1, 2, 3]
|
643
|
+
)
|
644
|
+
"""
|
645
|
+
if self.is_local:
|
646
|
+
return self.data.update_where(table, data, operator=operator, **filters)
|
647
|
+
else:
|
648
|
+
raise NotImplementedError("Remote bulk update not implemented")
|
649
|
+
|
499
650
|
|
500
651
|
def create_index(
|
501
652
|
self,
|
@@ -581,31 +732,6 @@ class CinchDB:
|
|
581
732
|
changes.append(Change(**data))
|
582
733
|
return changes
|
583
734
|
|
584
|
-
def optimize_tenant(self, tenant_name: str = None, force: bool = False) -> bool:
|
585
|
-
"""Optimize a tenant's storage with VACUUM and page size adjustment.
|
586
|
-
|
587
|
-
Args:
|
588
|
-
tenant_name: Name of the tenant to optimize (default: current tenant)
|
589
|
-
force: If True, always perform optimization
|
590
|
-
|
591
|
-
Returns:
|
592
|
-
True if optimization was performed, False otherwise
|
593
|
-
|
594
|
-
Examples:
|
595
|
-
# Optimize current tenant
|
596
|
-
db.optimize_tenant()
|
597
|
-
|
598
|
-
# Optimize specific tenant
|
599
|
-
db.optimize_tenant("store_west")
|
600
|
-
|
601
|
-
# Force optimization even if not needed
|
602
|
-
db.optimize_tenant(force=True)
|
603
|
-
"""
|
604
|
-
if self.is_local:
|
605
|
-
tenant_to_optimize = tenant_name or self.tenant
|
606
|
-
return self.tenants.optimize_tenant_storage(tenant_to_optimize, force=force)
|
607
|
-
else:
|
608
|
-
raise NotImplementedError("Remote tenant optimization not implemented")
|
609
735
|
|
610
736
|
def get_tenant_size(self, tenant_name: str = None) -> dict:
|
611
737
|
"""Get storage size information for a tenant.
|
@@ -639,6 +765,46 @@ class CinchDB:
|
|
639
765
|
else:
|
640
766
|
raise NotImplementedError("Remote tenant size query not implemented")
|
641
767
|
|
768
|
+
def vacuum_tenant(self, tenant_name: str = None) -> dict:
|
769
|
+
"""Run VACUUM operation on a specific tenant to optimize storage and performance.
|
770
|
+
|
771
|
+
VACUUM reclaims space from deleted records, defragments the database file,
|
772
|
+
and can improve query performance by rebuilding internal structures.
|
773
|
+
|
774
|
+
Args:
|
775
|
+
tenant_name: Name of tenant to vacuum (default: current tenant)
|
776
|
+
|
777
|
+
Returns:
|
778
|
+
Dictionary with vacuum results:
|
779
|
+
- success: Whether vacuum completed successfully
|
780
|
+
- tenant: Name of the tenant
|
781
|
+
- size_before: Size in bytes before vacuum
|
782
|
+
- size_after: Size in bytes after vacuum
|
783
|
+
- space_reclaimed: Bytes reclaimed by vacuum
|
784
|
+
- space_reclaimed_mb: MB reclaimed (rounded to 2 decimals)
|
785
|
+
- duration_seconds: Time taken for vacuum operation
|
786
|
+
- error: Error message if vacuum failed
|
787
|
+
|
788
|
+
Raises:
|
789
|
+
ValueError: If tenant doesn't exist or is not materialized
|
790
|
+
NotImplementedError: If called on remote database
|
791
|
+
|
792
|
+
Examples:
|
793
|
+
# Vacuum current tenant
|
794
|
+
result = db.vacuum_tenant()
|
795
|
+
if result['success']:
|
796
|
+
print(f"Reclaimed {result['space_reclaimed_mb']:.2f} MB")
|
797
|
+
|
798
|
+
# Vacuum specific tenant
|
799
|
+
result = db.vacuum_tenant("store_east")
|
800
|
+
print(f"Vacuum took {result['duration_seconds']} seconds")
|
801
|
+
"""
|
802
|
+
if self.is_local:
|
803
|
+
tenant_to_vacuum = tenant_name or self.tenant
|
804
|
+
return self.tenants.vacuum_tenant(tenant_to_vacuum)
|
805
|
+
else:
|
806
|
+
raise NotImplementedError("Remote tenant vacuum not implemented")
|
807
|
+
|
642
808
|
def get_storage_info(self) -> dict:
|
643
809
|
"""Get storage size information for all tenants in current branch.
|
644
810
|
|
@@ -667,35 +833,6 @@ class CinchDB:
|
|
667
833
|
else:
|
668
834
|
raise NotImplementedError("Remote storage info not implemented")
|
669
835
|
|
670
|
-
def optimize_all_tenants(self, force: bool = False) -> dict:
|
671
|
-
"""Optimize storage for all tenants in current branch.
|
672
|
-
|
673
|
-
This is designed to be called periodically to:
|
674
|
-
- Reclaim unused space with VACUUM
|
675
|
-
- Adjust page sizes as databases grow
|
676
|
-
- Keep small databases compact
|
677
|
-
|
678
|
-
Args:
|
679
|
-
force: If True, optimize all tenants regardless of size
|
680
|
-
|
681
|
-
Returns:
|
682
|
-
Dictionary with optimization results:
|
683
|
-
- optimized: List of tenant names that were optimized
|
684
|
-
- skipped: List of tenant names that were skipped
|
685
|
-
- errors: List of tuples (tenant_name, error_message)
|
686
|
-
|
687
|
-
Examples:
|
688
|
-
# Run periodic optimization
|
689
|
-
results = db.optimize_all_tenants()
|
690
|
-
print(f"Optimized {len(results['optimized'])} tenants")
|
691
|
-
|
692
|
-
# Force optimization of all tenants
|
693
|
-
results = db.optimize_all_tenants(force=True)
|
694
|
-
"""
|
695
|
-
if self.is_local:
|
696
|
-
return self.tenants.optimize_all_tenants(force=force)
|
697
|
-
else:
|
698
|
-
raise NotImplementedError("Remote tenant optimization not implemented")
|
699
836
|
|
700
837
|
def close(self):
|
701
838
|
"""Close any open connections."""
|
cinchdb/core/path_utils.py
CHANGED
@@ -4,7 +4,6 @@ import sqlite3
|
|
4
4
|
import uuid
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import Optional, List, Dict, Any
|
7
|
-
from datetime import datetime
|
8
7
|
import json
|
9
8
|
|
10
9
|
|
@@ -117,6 +116,21 @@ class MetadataDB:
|
|
117
116
|
CREATE INDEX IF NOT EXISTS idx_tenants_shard
|
118
117
|
ON tenants(shard)
|
119
118
|
""")
|
119
|
+
|
120
|
+
# Add cdc_enabled field to branches table for plugin use
|
121
|
+
# Note: This field is managed by plugins (like bdhcnic), not core CinchDB
|
122
|
+
try:
|
123
|
+
self.conn.execute("""
|
124
|
+
ALTER TABLE branches ADD COLUMN cdc_enabled BOOLEAN DEFAULT FALSE
|
125
|
+
""")
|
126
|
+
except sqlite3.OperationalError:
|
127
|
+
# Column already exists
|
128
|
+
pass
|
129
|
+
|
130
|
+
self.conn.execute("""
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_branches_cdc_enabled
|
132
|
+
ON branches(cdc_enabled)
|
133
|
+
""")
|
120
134
|
|
121
135
|
# Database operations
|
122
136
|
def create_database(self, database_id: str, name: str,
|
cinchdb/managers/branch.py
CHANGED
cinchdb/managers/data.py
CHANGED
@@ -283,8 +283,8 @@ class DataManager:
|
|
283
283
|
conn.rollback()
|
284
284
|
raise
|
285
285
|
|
286
|
-
def
|
287
|
-
"""Delete a single record by ID.
|
286
|
+
def delete_model_by_id(self, model_class: Type[T], record_id: str) -> bool:
|
287
|
+
"""Delete a single record by ID using model class.
|
288
288
|
|
289
289
|
Args:
|
290
290
|
model_class: Pydantic model class representing the table
|
@@ -420,12 +420,13 @@ class DataManager:
|
|
420
420
|
return snake_case
|
421
421
|
|
422
422
|
def _build_where_clause(
|
423
|
-
self, filters: Dict[str, Any]
|
423
|
+
self, filters: Dict[str, Any], operator: str = "AND"
|
424
424
|
) -> tuple[str, Dict[str, Any]]:
|
425
425
|
"""Build WHERE clause and parameters from filters.
|
426
426
|
|
427
427
|
Args:
|
428
428
|
filters: Dictionary of column filters
|
429
|
+
operator: Logical operator to combine conditions - "AND" (default) or "OR"
|
429
430
|
|
430
431
|
Returns:
|
431
432
|
Tuple of (where_clause, parameters)
|
@@ -433,31 +434,34 @@ class DataManager:
|
|
433
434
|
if not filters:
|
434
435
|
return "", {}
|
435
436
|
|
437
|
+
if operator not in ("AND", "OR"):
|
438
|
+
raise ValueError(f"Operator must be 'AND' or 'OR', got: {operator}")
|
439
|
+
|
436
440
|
conditions = []
|
437
441
|
params = {}
|
438
442
|
|
439
443
|
for key, value in filters.items():
|
440
444
|
# Handle special operators (column__operator format)
|
441
445
|
if "__" in key:
|
442
|
-
column,
|
443
|
-
param_key = f"{column}_{
|
446
|
+
column, op = key.split("__", 1)
|
447
|
+
param_key = f"{column}_{op}"
|
444
448
|
|
445
|
-
if
|
449
|
+
if op == "gte":
|
446
450
|
conditions.append(f"{column} >= :{param_key}")
|
447
451
|
params[param_key] = value
|
448
|
-
elif
|
452
|
+
elif op == "lte":
|
449
453
|
conditions.append(f"{column} <= :{param_key}")
|
450
454
|
params[param_key] = value
|
451
|
-
elif
|
455
|
+
elif op == "gt":
|
452
456
|
conditions.append(f"{column} > :{param_key}")
|
453
457
|
params[param_key] = value
|
454
|
-
elif
|
458
|
+
elif op == "lt":
|
455
459
|
conditions.append(f"{column} < :{param_key}")
|
456
460
|
params[param_key] = value
|
457
|
-
elif
|
461
|
+
elif op == "like":
|
458
462
|
conditions.append(f"{column} LIKE :{param_key}")
|
459
463
|
params[param_key] = value
|
460
|
-
elif
|
464
|
+
elif op == "in":
|
461
465
|
if not isinstance(value, (list, tuple)):
|
462
466
|
raise ValueError(
|
463
467
|
f"'in' operator requires list or tuple, got {type(value)}"
|
@@ -467,10 +471,182 @@ class DataManager:
|
|
467
471
|
for i, v in enumerate(value):
|
468
472
|
params[f"{param_key}_{i}"] = v
|
469
473
|
else:
|
470
|
-
raise ValueError(f"Unsupported operator: {
|
474
|
+
raise ValueError(f"Unsupported operator: {op}")
|
471
475
|
else:
|
472
476
|
# Exact match
|
473
477
|
conditions.append(f"{key} = :{key}")
|
474
478
|
params[key] = value
|
475
479
|
|
476
|
-
return "
|
480
|
+
return f" {operator} ".join(conditions), params
|
481
|
+
|
482
|
+
def delete_where(self, table: str, operator: str = "AND", **filters) -> int:
|
483
|
+
"""Delete records from a table based on filter criteria.
|
484
|
+
|
485
|
+
Args:
|
486
|
+
table: Table name
|
487
|
+
operator: Logical operator to combine conditions - "AND" (default) or "OR"
|
488
|
+
**filters: Filter criteria (supports operators like __gt, __lt, __in, __like, __not)
|
489
|
+
Multiple conditions are combined with the specified operator
|
490
|
+
|
491
|
+
Returns:
|
492
|
+
Number of records deleted
|
493
|
+
|
494
|
+
Examples:
|
495
|
+
# Delete records where status = 'inactive'
|
496
|
+
count = dm.delete_where('users', status='inactive')
|
497
|
+
|
498
|
+
# Delete records where status = 'inactive' AND age > 65 (default AND)
|
499
|
+
count = dm.delete_where('users', status='inactive', age__gt=65)
|
500
|
+
|
501
|
+
# Delete records where status = 'inactive' OR age > 65
|
502
|
+
count = dm.delete_where('users', operator='OR', status='inactive', age__gt=65)
|
503
|
+
|
504
|
+
# Delete records where age > 65
|
505
|
+
count = dm.delete_where('users', age__gt=65)
|
506
|
+
|
507
|
+
# Delete records where id in [1, 2, 3]
|
508
|
+
count = dm.delete_where('users', id__in=[1, 2, 3])
|
509
|
+
|
510
|
+
# Delete records where name like 'test%'
|
511
|
+
count = dm.delete_where('users', name__like='test%')
|
512
|
+
"""
|
513
|
+
# Check maintenance mode
|
514
|
+
check_maintenance_mode(self.project_root, self.database, self.branch)
|
515
|
+
|
516
|
+
if not filters:
|
517
|
+
raise ValueError("delete_where requires at least one filter condition")
|
518
|
+
|
519
|
+
# Build WHERE clause
|
520
|
+
where_clause, params = self._build_where_clause(filters, operator)
|
521
|
+
|
522
|
+
sql = f"DELETE FROM {table} WHERE {where_clause}"
|
523
|
+
|
524
|
+
with DatabaseConnection(self.db_path) as conn:
|
525
|
+
cursor = conn.execute(sql, params)
|
526
|
+
conn.commit()
|
527
|
+
return cursor.rowcount
|
528
|
+
|
529
|
+
def update_where(self, table: str, data: Dict[str, Any], operator: str = "AND", **filters) -> int:
|
530
|
+
"""Update records in a table based on filter criteria.
|
531
|
+
|
532
|
+
Args:
|
533
|
+
table: Table name
|
534
|
+
data: Dictionary of column-value pairs to update
|
535
|
+
operator: Logical operator to combine conditions - "AND" (default) or "OR"
|
536
|
+
**filters: Filter criteria (supports operators like __gt, __lt, __in, __like, __not)
|
537
|
+
Multiple conditions are combined with the specified operator
|
538
|
+
|
539
|
+
Returns:
|
540
|
+
Number of records updated
|
541
|
+
|
542
|
+
Examples:
|
543
|
+
# Update status for all users with age > 65
|
544
|
+
count = dm.update_where('users', {'status': 'senior'}, age__gt=65)
|
545
|
+
|
546
|
+
# Update status where age > 65 AND status = 'active' (default AND)
|
547
|
+
count = dm.update_where('users', {'status': 'senior'}, age__gt=65, status='active')
|
548
|
+
|
549
|
+
# Update status where age > 65 OR status = 'pending'
|
550
|
+
count = dm.update_where('users', {'status': 'senior'}, operator='OR', age__gt=65, status='pending')
|
551
|
+
|
552
|
+
# Update multiple fields where id in specific list
|
553
|
+
count = dm.update_where(
|
554
|
+
'users',
|
555
|
+
{'status': 'inactive', 'updated_at': datetime.now()},
|
556
|
+
id__in=[1, 2, 3]
|
557
|
+
)
|
558
|
+
|
559
|
+
# Update where name like pattern
|
560
|
+
count = dm.update_where('users', {'category': 'test'}, name__like='test%')
|
561
|
+
"""
|
562
|
+
# Check maintenance mode
|
563
|
+
check_maintenance_mode(self.project_root, self.database, self.branch)
|
564
|
+
|
565
|
+
if not filters:
|
566
|
+
raise ValueError("update_where requires at least one filter condition")
|
567
|
+
|
568
|
+
if not data:
|
569
|
+
raise ValueError("update_where requires data to update")
|
570
|
+
|
571
|
+
# Build WHERE clause
|
572
|
+
where_clause, where_params = self._build_where_clause(filters, operator)
|
573
|
+
|
574
|
+
# Build SET clause
|
575
|
+
set_clauses = []
|
576
|
+
update_params = {}
|
577
|
+
|
578
|
+
for key, value in data.items():
|
579
|
+
param_key = f"update_{key}"
|
580
|
+
set_clauses.append(f"{key} = :{param_key}")
|
581
|
+
update_params[param_key] = value
|
582
|
+
|
583
|
+
# Combine parameters (update params first to avoid conflicts)
|
584
|
+
all_params = {**update_params, **where_params}
|
585
|
+
|
586
|
+
sql = f"UPDATE {table} SET {', '.join(set_clauses)} WHERE {where_clause}"
|
587
|
+
|
588
|
+
with DatabaseConnection(self.db_path) as conn:
|
589
|
+
cursor = conn.execute(sql, all_params)
|
590
|
+
conn.commit()
|
591
|
+
return cursor.rowcount
|
592
|
+
|
593
|
+
def update_by_id(self, table: str, record_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
594
|
+
"""Update a single record by ID.
|
595
|
+
|
596
|
+
Args:
|
597
|
+
table: Table name
|
598
|
+
record_id: Record ID to update
|
599
|
+
data: Dictionary of column-value pairs to update
|
600
|
+
|
601
|
+
Returns:
|
602
|
+
Updated record
|
603
|
+
"""
|
604
|
+
# Build SET clause
|
605
|
+
set_parts = []
|
606
|
+
params = []
|
607
|
+
|
608
|
+
for key, value in data.items():
|
609
|
+
set_parts.append(f"{key} = ?")
|
610
|
+
params.append(value)
|
611
|
+
|
612
|
+
if not set_parts:
|
613
|
+
raise ValueError("No data provided for update")
|
614
|
+
|
615
|
+
set_clause = ", ".join(set_parts)
|
616
|
+
sql = f"UPDATE {table} SET {set_clause} WHERE id = ?"
|
617
|
+
params.append(record_id)
|
618
|
+
|
619
|
+
with DatabaseConnection(self.db_path) as conn:
|
620
|
+
cursor = conn.execute(sql, params)
|
621
|
+
conn.commit()
|
622
|
+
|
623
|
+
if cursor.rowcount == 0:
|
624
|
+
raise ValueError(f"No record found with id: {record_id}")
|
625
|
+
|
626
|
+
# Return the updated record
|
627
|
+
result = conn.execute(f"SELECT * FROM {table} WHERE id = ?", [record_id])
|
628
|
+
row = result.fetchone()
|
629
|
+
if row:
|
630
|
+
return dict(row)
|
631
|
+
else:
|
632
|
+
raise ValueError(f"Record not found after update: {record_id}")
|
633
|
+
|
634
|
+
def delete_by_id(self, table: str, record_id: str) -> bool:
|
635
|
+
"""Delete a single record by ID using table name.
|
636
|
+
|
637
|
+
This method is used by the high-level database API.
|
638
|
+
|
639
|
+
Args:
|
640
|
+
table: Table name
|
641
|
+
record_id: Record ID to delete
|
642
|
+
|
643
|
+
Returns:
|
644
|
+
True if record was deleted, False if not found
|
645
|
+
"""
|
646
|
+
sql = f"DELETE FROM {table} WHERE id = ?"
|
647
|
+
|
648
|
+
with DatabaseConnection(self.db_path) as conn:
|
649
|
+
cursor = conn.execute(sql, [record_id])
|
650
|
+
conn.commit()
|
651
|
+
return cursor.rowcount > 0
|
652
|
+
|
cinchdb/managers/index.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
from pathlib import Path
|
4
4
|
from typing import List, Dict, Any, Optional
|
5
5
|
import sqlite3
|
6
|
-
import json
|
7
6
|
from datetime import datetime, timezone
|
8
7
|
import uuid
|
9
8
|
|
@@ -263,7 +262,7 @@ class IndexManager:
|
|
263
262
|
|
264
263
|
# Get index statistics
|
265
264
|
xinfo_result = conn.execute(f"PRAGMA index_xinfo({index_name})")
|
266
|
-
|
265
|
+
xinfo_result.fetchall()
|
267
266
|
|
268
267
|
return {
|
269
268
|
"name": index_name,
|
cinchdb/managers/query.py
CHANGED
@@ -6,7 +6,6 @@ from typing import List, Dict, Any, Optional, Type, TypeVar, Union
|
|
6
6
|
from pydantic import BaseModel, ValidationError
|
7
7
|
|
8
8
|
from cinchdb.core.connection import DatabaseConnection
|
9
|
-
from cinchdb.core.path_utils import get_tenant_db_path
|
10
9
|
from cinchdb.utils import validate_query_safe
|
11
10
|
from cinchdb.managers.tenant import TenantManager
|
12
11
|
|