cinchdb 0.1.15__py3-none-any.whl → 0.1.18__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/cli/commands/tenant.py +50 -2
- cinchdb/config.py +4 -13
- cinchdb/core/connection.py +57 -19
- cinchdb/core/database.py +39 -12
- cinchdb/core/initializer.py +25 -40
- cinchdb/core/maintenance_utils.py +43 -0
- cinchdb/core/path_utils.py +211 -38
- cinchdb/core/tenant_activation.py +216 -0
- cinchdb/infrastructure/metadata_db.py +96 -3
- cinchdb/managers/change_applier.py +22 -23
- cinchdb/managers/codegen.py +372 -15
- cinchdb/managers/column.py +8 -5
- cinchdb/managers/data.py +17 -14
- cinchdb/managers/query.py +8 -5
- cinchdb/managers/table.py +9 -6
- cinchdb/managers/tenant.py +228 -72
- cinchdb/managers/view.py +1 -1
- cinchdb/plugins/__init__.py +7 -8
- cinchdb/plugins/base.py +55 -74
- cinchdb/plugins/decorators.py +36 -32
- cinchdb/plugins/manager.py +103 -71
- cinchdb/utils/name_validator.py +22 -12
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.18.dist-info}/METADATA +39 -1
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.18.dist-info}/RECORD +27 -26
- cinchdb/core/maintenance.py +0 -73
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.18.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.18.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.18.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +10,8 @@ from cinchdb.models import Change, ChangeType, Tenant
|
|
10
10
|
from cinchdb.managers.change_tracker import ChangeTracker
|
11
11
|
from cinchdb.managers.tenant import TenantManager
|
12
12
|
from cinchdb.core.connection import DatabaseConnection
|
13
|
-
from cinchdb.core.path_utils import get_tenant_db_path, get_branch_path
|
13
|
+
from cinchdb.core.path_utils import get_tenant_db_path as get_tenant_db_path, get_branch_path
|
14
|
+
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
14
15
|
|
15
16
|
logger = logging.getLogger(__name__)
|
16
17
|
|
@@ -388,21 +389,17 @@ class ChangeApplier:
|
|
388
389
|
|
389
390
|
def _enter_maintenance_mode(self) -> None:
|
390
391
|
"""Enter maintenance mode to block writes during schema changes."""
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
json.dump(
|
399
|
-
{
|
400
|
-
"active": True,
|
401
|
-
"reason": "Schema update in progress",
|
402
|
-
"started_at": datetime.now().isoformat(),
|
403
|
-
},
|
404
|
-
f,
|
392
|
+
try:
|
393
|
+
metadata_db = get_metadata_db(self.project_root)
|
394
|
+
metadata_db.set_branch_maintenance(
|
395
|
+
self.database,
|
396
|
+
self.branch,
|
397
|
+
True,
|
398
|
+
"Schema update in progress"
|
405
399
|
)
|
400
|
+
except Exception as e:
|
401
|
+
logger.error(f"Failed to enter maintenance mode: {e}")
|
402
|
+
# Continue anyway - we'll try to proceed with the schema update
|
406
403
|
|
407
404
|
# Give time for any in-flight writes to complete
|
408
405
|
# Can be disabled for tests via environment variable
|
@@ -414,11 +411,11 @@ class ChangeApplier:
|
|
414
411
|
|
415
412
|
def _exit_maintenance_mode(self) -> None:
|
416
413
|
"""Exit maintenance mode to allow writes again."""
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
414
|
+
try:
|
415
|
+
metadata_db = get_metadata_db(self.project_root)
|
416
|
+
metadata_db.set_branch_maintenance(self.database, self.branch, False)
|
417
|
+
except Exception as e:
|
418
|
+
logger.error(f"Failed to exit maintenance mode: {e}")
|
422
419
|
|
423
420
|
def is_in_maintenance_mode(self) -> bool:
|
424
421
|
"""Check if branch is in maintenance mode.
|
@@ -426,6 +423,8 @@ class ChangeApplier:
|
|
426
423
|
Returns:
|
427
424
|
True if in maintenance mode, False otherwise
|
428
425
|
"""
|
429
|
-
|
430
|
-
|
431
|
-
|
426
|
+
try:
|
427
|
+
metadata_db = get_metadata_db(self.project_root)
|
428
|
+
return metadata_db.is_branch_in_maintenance(self.database, self.branch)
|
429
|
+
except Exception:
|
430
|
+
return False
|
cinchdb/managers/codegen.py
CHANGED
@@ -379,21 +379,58 @@ class CodegenManager:
|
|
379
379
|
results: Dict[str, Any],
|
380
380
|
) -> Dict[str, Any]:
|
381
381
|
"""Generate TypeScript interface models."""
|
382
|
-
#
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
)
|
395
|
-
|
396
|
-
|
382
|
+
# Generate interfaces for tables
|
383
|
+
if include_tables:
|
384
|
+
tables = self.table_manager.list_tables()
|
385
|
+
for table in tables:
|
386
|
+
interface_content = self._generate_typescript_table_interface(table)
|
387
|
+
file_name = f"{self._to_pascal_case(table.name)}.ts"
|
388
|
+
file_path = output_dir / file_name
|
389
|
+
|
390
|
+
with open(file_path, "w") as f:
|
391
|
+
f.write(interface_content)
|
392
|
+
|
393
|
+
results["tables_processed"].append(table.name)
|
394
|
+
results["files_generated"].append(file_name)
|
395
|
+
|
396
|
+
# Generate interfaces for views
|
397
|
+
if include_views:
|
398
|
+
views = self.view_manager.list_views()
|
399
|
+
for view in views:
|
400
|
+
interface_content = self._generate_typescript_view_interface(view)
|
401
|
+
file_name = f"{self._to_pascal_case(view.name)}View.ts"
|
402
|
+
file_path = output_dir / file_name
|
403
|
+
|
404
|
+
with open(file_path, "w") as f:
|
405
|
+
f.write(interface_content)
|
406
|
+
|
407
|
+
results["views_processed"].append(view.name)
|
408
|
+
results["files_generated"].append(file_name)
|
409
|
+
|
410
|
+
# Generate main index file
|
411
|
+
index_content = self._generate_typescript_index(
|
412
|
+
results["tables_processed"],
|
413
|
+
results["views_processed"]
|
414
|
+
)
|
415
|
+
index_path = output_dir / "index.ts"
|
416
|
+
with open(index_path, "w") as f:
|
417
|
+
f.write(index_content)
|
418
|
+
results["files_generated"].append("index.ts")
|
419
|
+
|
420
|
+
# Generate API client
|
421
|
+
client_content = self._generate_typescript_client()
|
422
|
+
client_path = output_dir / "client.ts"
|
423
|
+
with open(client_path, "w") as f:
|
424
|
+
f.write(client_content)
|
425
|
+
results["files_generated"].append("client.ts")
|
426
|
+
|
427
|
+
# Generate types file
|
428
|
+
types_content = self._generate_typescript_types()
|
429
|
+
types_path = output_dir / "types.ts"
|
430
|
+
with open(types_path, "w") as f:
|
431
|
+
f.write(types_content)
|
432
|
+
results["files_generated"].append("types.ts")
|
433
|
+
|
397
434
|
return results
|
398
435
|
|
399
436
|
def _get_view_columns(self, view_name: str) -> Dict[str, str]:
|
@@ -454,6 +491,326 @@ class CodegenManager:
|
|
454
491
|
# Split on underscores and capitalize each part
|
455
492
|
parts = name.replace("-", "_").split("_")
|
456
493
|
return "".join(word.capitalize() for word in parts if word)
|
494
|
+
|
495
|
+
def _sqlite_to_typescript_type(self, sqlite_type: str, column_name: str = "") -> str:
|
496
|
+
"""Convert SQLite type to TypeScript type string."""
|
497
|
+
sqlite_type = sqlite_type.upper()
|
498
|
+
|
499
|
+
# Special case for timestamp fields
|
500
|
+
if column_name in ["created_at", "updated_at"]:
|
501
|
+
return "string" # ISO date strings
|
502
|
+
|
503
|
+
if "INT" in sqlite_type:
|
504
|
+
return "number"
|
505
|
+
elif sqlite_type in ["REAL", "FLOAT", "DOUBLE", "NUMERIC"]:
|
506
|
+
return "number"
|
507
|
+
elif sqlite_type == "BLOB":
|
508
|
+
return "Uint8Array"
|
509
|
+
elif sqlite_type == "BOOLEAN":
|
510
|
+
return "boolean"
|
511
|
+
else:
|
512
|
+
# TEXT, VARCHAR, etc.
|
513
|
+
return "string"
|
514
|
+
|
515
|
+
def _generate_typescript_table_interface(self, table: Table) -> str:
|
516
|
+
"""Generate TypeScript interface for a table."""
|
517
|
+
interface_name = self._to_pascal_case(table.name)
|
518
|
+
|
519
|
+
content = [
|
520
|
+
f"/**",
|
521
|
+
f" * Generated interface for {table.name} table",
|
522
|
+
f" */",
|
523
|
+
f"export interface {interface_name} {{",
|
524
|
+
]
|
525
|
+
|
526
|
+
# Generate properties for each column
|
527
|
+
for column in table.columns:
|
528
|
+
ts_type = self._sqlite_to_typescript_type(column.type, column.name)
|
529
|
+
optional = "?" if column.nullable and column.name not in ["id", "created_at"] else ""
|
530
|
+
content.append(f" {column.name}{optional}: {ts_type};")
|
531
|
+
|
532
|
+
content.append("}")
|
533
|
+
content.append("")
|
534
|
+
|
535
|
+
# Generate input interface (for create/update operations)
|
536
|
+
content.extend([
|
537
|
+
f"/**",
|
538
|
+
f" * Input interface for creating/updating {table.name} records",
|
539
|
+
f" */",
|
540
|
+
f"export interface {interface_name}Input {{",
|
541
|
+
])
|
542
|
+
|
543
|
+
for column in table.columns:
|
544
|
+
# Skip auto-generated fields in input
|
545
|
+
if column.name in ["id", "created_at", "updated_at"]:
|
546
|
+
continue
|
547
|
+
ts_type = self._sqlite_to_typescript_type(column.type, column.name)
|
548
|
+
optional = "?" if column.nullable else ""
|
549
|
+
content.append(f" {column.name}{optional}: {ts_type};")
|
550
|
+
|
551
|
+
content.append("}")
|
552
|
+
content.append("")
|
553
|
+
|
554
|
+
return "\n".join(content)
|
555
|
+
|
556
|
+
def _generate_typescript_view_interface(self, view: View) -> str:
|
557
|
+
"""Generate TypeScript interface for a view."""
|
558
|
+
interface_name = f"{self._to_pascal_case(view.name)}View"
|
559
|
+
|
560
|
+
# Get view columns from PRAGMA
|
561
|
+
columns = self._get_view_columns(view.name)
|
562
|
+
|
563
|
+
content = [
|
564
|
+
f"/**",
|
565
|
+
f" * Generated interface for {view.name} view (read-only)",
|
566
|
+
f" */",
|
567
|
+
f"export interface {interface_name} {{",
|
568
|
+
]
|
569
|
+
|
570
|
+
# Generate properties for each column
|
571
|
+
for column_name, column_type in columns.items():
|
572
|
+
ts_type = self._sqlite_to_typescript_type(column_type, column_name)
|
573
|
+
content.append(f" {column_name}: {ts_type};")
|
574
|
+
|
575
|
+
content.append("}")
|
576
|
+
content.append("")
|
577
|
+
|
578
|
+
return "\n".join(content)
|
579
|
+
|
580
|
+
def _generate_typescript_index(self, tables: List[str], views: List[str]) -> str:
|
581
|
+
"""Generate TypeScript index file exporting all interfaces."""
|
582
|
+
content = [
|
583
|
+
"/**",
|
584
|
+
" * Generated TypeScript models for CinchDB",
|
585
|
+
" */",
|
586
|
+
"",
|
587
|
+
]
|
588
|
+
|
589
|
+
# Export tables
|
590
|
+
for table_name in tables:
|
591
|
+
interface_name = self._to_pascal_case(table_name)
|
592
|
+
content.append(f"export {{ {interface_name}, {interface_name}Input }} from './{interface_name}';")
|
593
|
+
|
594
|
+
# Export views
|
595
|
+
for view_name in views:
|
596
|
+
interface_name = f"{self._to_pascal_case(view_name)}View"
|
597
|
+
content.append(f"export {{ {interface_name} }} from './{interface_name}';")
|
598
|
+
|
599
|
+
# Export client and types
|
600
|
+
content.extend([
|
601
|
+
"",
|
602
|
+
"export { CinchDBClient } from './client';",
|
603
|
+
"export * from './types';",
|
604
|
+
"",
|
605
|
+
])
|
606
|
+
|
607
|
+
return "\n".join(content)
|
608
|
+
|
609
|
+
def _generate_typescript_client(self) -> str:
|
610
|
+
"""Generate TypeScript API client class."""
|
611
|
+
content = [
|
612
|
+
"/**",
|
613
|
+
" * CinchDB TypeScript API Client",
|
614
|
+
" */",
|
615
|
+
"",
|
616
|
+
"import { QueryResult, CreateResult, UpdateResult, DeleteResult } from './types';",
|
617
|
+
"",
|
618
|
+
"export class CinchDBClient {",
|
619
|
+
" private baseUrl: string;",
|
620
|
+
" private headers: HeadersInit;",
|
621
|
+
"",
|
622
|
+
" constructor(baseUrl: string, apiKey: string) {",
|
623
|
+
" this.baseUrl = baseUrl;",
|
624
|
+
" this.headers = {",
|
625
|
+
" 'Content-Type': 'application/json',",
|
626
|
+
" 'X-API-Key': apiKey,",
|
627
|
+
" };",
|
628
|
+
" }",
|
629
|
+
"",
|
630
|
+
" /**",
|
631
|
+
" * Execute a query against the database",
|
632
|
+
" */",
|
633
|
+
" async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {",
|
634
|
+
" const response = await fetch(`${this.baseUrl}/api/v1/query`, {",
|
635
|
+
" method: 'POST',",
|
636
|
+
" headers: this.headers,",
|
637
|
+
" body: JSON.stringify({ sql, params }),",
|
638
|
+
" });",
|
639
|
+
"",
|
640
|
+
" if (!response.ok) {",
|
641
|
+
" throw new Error(`Query failed: ${response.statusText}`);",
|
642
|
+
" }",
|
643
|
+
"",
|
644
|
+
" return response.json();",
|
645
|
+
" }",
|
646
|
+
"",
|
647
|
+
" /**",
|
648
|
+
" * Select records from a table",
|
649
|
+
" */",
|
650
|
+
" async select<T = any>(",
|
651
|
+
" table: string,",
|
652
|
+
" filters?: Record<string, any>,",
|
653
|
+
" limit?: number,",
|
654
|
+
" offset?: number",
|
655
|
+
" ): Promise<T[]> {",
|
656
|
+
" const params = new URLSearchParams();",
|
657
|
+
" if (filters) {",
|
658
|
+
" Object.entries(filters).forEach(([key, value]) => {",
|
659
|
+
" params.append(key, String(value));",
|
660
|
+
" });",
|
661
|
+
" }",
|
662
|
+
" if (limit !== undefined) params.append('limit', String(limit));",
|
663
|
+
" if (offset !== undefined) params.append('offset', String(offset));",
|
664
|
+
"",
|
665
|
+
" const response = await fetch(",
|
666
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records?${params}`,",
|
667
|
+
" {",
|
668
|
+
" method: 'GET',",
|
669
|
+
" headers: this.headers,",
|
670
|
+
" }",
|
671
|
+
" );",
|
672
|
+
"",
|
673
|
+
" if (!response.ok) {",
|
674
|
+
" throw new Error(`Select failed: ${response.statusText}`);",
|
675
|
+
" }",
|
676
|
+
"",
|
677
|
+
" const result = await response.json();",
|
678
|
+
" return result.records;",
|
679
|
+
" }",
|
680
|
+
"",
|
681
|
+
" /**",
|
682
|
+
" * Create a new record",
|
683
|
+
" */",
|
684
|
+
" async create<T = any>(table: string, data: Partial<T>): Promise<CreateResult<T>> {",
|
685
|
+
" const response = await fetch(`${this.baseUrl}/api/v1/tables/${table}/records`, {",
|
686
|
+
" method: 'POST',",
|
687
|
+
" headers: this.headers,",
|
688
|
+
" body: JSON.stringify(data),",
|
689
|
+
" });",
|
690
|
+
"",
|
691
|
+
" if (!response.ok) {",
|
692
|
+
" throw new Error(`Create failed: ${response.statusText}`);",
|
693
|
+
" }",
|
694
|
+
"",
|
695
|
+
" return response.json();",
|
696
|
+
" }",
|
697
|
+
"",
|
698
|
+
" /**",
|
699
|
+
" * Update a record by ID",
|
700
|
+
" */",
|
701
|
+
" async update<T = any>(",
|
702
|
+
" table: string,",
|
703
|
+
" id: string,",
|
704
|
+
" data: Partial<T>",
|
705
|
+
" ): Promise<UpdateResult<T>> {",
|
706
|
+
" const response = await fetch(",
|
707
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/${id}`,",
|
708
|
+
" {",
|
709
|
+
" method: 'PUT',",
|
710
|
+
" headers: this.headers,",
|
711
|
+
" body: JSON.stringify(data),",
|
712
|
+
" }",
|
713
|
+
" );",
|
714
|
+
"",
|
715
|
+
" if (!response.ok) {",
|
716
|
+
" throw new Error(`Update failed: ${response.statusText}`);",
|
717
|
+
" }",
|
718
|
+
"",
|
719
|
+
" return response.json();",
|
720
|
+
" }",
|
721
|
+
"",
|
722
|
+
" /**",
|
723
|
+
" * Delete a record by ID",
|
724
|
+
" */",
|
725
|
+
" async delete(table: string, id: string): Promise<DeleteResult> {",
|
726
|
+
" const response = await fetch(",
|
727
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/${id}`,",
|
728
|
+
" {",
|
729
|
+
" method: 'DELETE',",
|
730
|
+
" headers: this.headers,",
|
731
|
+
" }",
|
732
|
+
" );",
|
733
|
+
"",
|
734
|
+
" if (!response.ok) {",
|
735
|
+
" throw new Error(`Delete failed: ${response.statusText}`);",
|
736
|
+
" }",
|
737
|
+
"",
|
738
|
+
" return response.json();",
|
739
|
+
" }",
|
740
|
+
"",
|
741
|
+
" /**",
|
742
|
+
" * Bulk create multiple records",
|
743
|
+
" */",
|
744
|
+
" async bulkCreate<T = any>(",
|
745
|
+
" table: string,",
|
746
|
+
" records: Partial<T>[]",
|
747
|
+
" ): Promise<CreateResult<T>[]> {",
|
748
|
+
" const response = await fetch(",
|
749
|
+
" `${this.baseUrl}/api/v1/tables/${table}/records/bulk`,",
|
750
|
+
" {",
|
751
|
+
" method: 'POST',",
|
752
|
+
" headers: this.headers,",
|
753
|
+
" body: JSON.stringify({ records }),",
|
754
|
+
" }",
|
755
|
+
" );",
|
756
|
+
"",
|
757
|
+
" if (!response.ok) {",
|
758
|
+
" throw new Error(`Bulk create failed: ${response.statusText}`);",
|
759
|
+
" }",
|
760
|
+
"",
|
761
|
+
" return response.json();",
|
762
|
+
" }",
|
763
|
+
"}",
|
764
|
+
"",
|
765
|
+
]
|
766
|
+
|
767
|
+
return "\n".join(content)
|
768
|
+
|
769
|
+
def _generate_typescript_types(self) -> str:
|
770
|
+
"""Generate TypeScript type definitions."""
|
771
|
+
content = [
|
772
|
+
"/**",
|
773
|
+
" * Common TypeScript type definitions for CinchDB",
|
774
|
+
" */",
|
775
|
+
"",
|
776
|
+
"export interface QueryResult<T = any> {",
|
777
|
+
" success: boolean;",
|
778
|
+
" data: T[];",
|
779
|
+
" rowCount: number;",
|
780
|
+
" error?: string;",
|
781
|
+
"}",
|
782
|
+
"",
|
783
|
+
"export interface CreateResult<T = any> {",
|
784
|
+
" success: boolean;",
|
785
|
+
" data: T;",
|
786
|
+
" error?: string;",
|
787
|
+
"}",
|
788
|
+
"",
|
789
|
+
"export interface UpdateResult<T = any> {",
|
790
|
+
" success: boolean;",
|
791
|
+
" data: T;",
|
792
|
+
" rowsAffected: number;",
|
793
|
+
" error?: string;",
|
794
|
+
"}",
|
795
|
+
"",
|
796
|
+
"export interface DeleteResult {",
|
797
|
+
" success: boolean;",
|
798
|
+
" rowsAffected: number;",
|
799
|
+
" error?: string;",
|
800
|
+
"}",
|
801
|
+
"",
|
802
|
+
"export interface PaginationParams {",
|
803
|
+
" limit?: number;",
|
804
|
+
" offset?: number;",
|
805
|
+
"}",
|
806
|
+
"",
|
807
|
+
"export interface FilterParams {",
|
808
|
+
" [key: string]: any;",
|
809
|
+
"}",
|
810
|
+
"",
|
811
|
+
]
|
812
|
+
|
813
|
+
return "\n".join(content)
|
457
814
|
|
458
815
|
def _generate_cinch_models_class(self) -> str:
|
459
816
|
"""Generate the CinchModels container class."""
|
cinchdb/managers/column.py
CHANGED
@@ -6,7 +6,7 @@ from typing import List, Any, Optional
|
|
6
6
|
from cinchdb.models import Column, Change, ChangeType
|
7
7
|
from cinchdb.core.connection import DatabaseConnection
|
8
8
|
from cinchdb.core.path_utils import get_tenant_db_path
|
9
|
-
from cinchdb.core.
|
9
|
+
from cinchdb.core.maintenance_utils import check_maintenance_mode
|
10
10
|
from cinchdb.managers.change_tracker import ChangeTracker
|
11
11
|
from cinchdb.managers.table import TableManager
|
12
12
|
|
@@ -18,7 +18,8 @@ class ColumnManager:
|
|
18
18
|
PROTECTED_COLUMNS = {"id", "created_at", "updated_at"}
|
19
19
|
|
20
20
|
def __init__(
|
21
|
-
self, project_root: Path, database: str, branch: str, tenant: str = "main"
|
21
|
+
self, project_root: Path, database: str, branch: str, tenant: str = "main",
|
22
|
+
encryption_manager=None
|
22
23
|
):
|
23
24
|
"""Initialize column manager.
|
24
25
|
|
@@ -27,14 +28,16 @@ class ColumnManager:
|
|
27
28
|
database: Database name
|
28
29
|
branch: Branch name
|
29
30
|
tenant: Tenant name (default: main)
|
31
|
+
encryption_manager: EncryptionManager instance for encrypted connections
|
30
32
|
"""
|
31
33
|
self.project_root = Path(project_root)
|
32
34
|
self.database = database
|
33
35
|
self.branch = branch
|
34
36
|
self.tenant = tenant
|
37
|
+
self.encryption_manager = encryption_manager
|
35
38
|
self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
|
36
39
|
self.change_tracker = ChangeTracker(project_root, database, branch)
|
37
|
-
self.table_manager = TableManager(project_root, database, branch, tenant)
|
40
|
+
self.table_manager = TableManager(project_root, database, branch, tenant, encryption_manager)
|
38
41
|
|
39
42
|
def list_columns(self, table_name: str) -> List[Column]:
|
40
43
|
"""List all columns in a table.
|
@@ -415,7 +418,7 @@ class ColumnManager:
|
|
415
418
|
|
416
419
|
create_sql = f"CREATE TABLE {temp_table} ({', '.join(col_defs)})"
|
417
420
|
|
418
|
-
with DatabaseConnection(self.db_path) as conn:
|
421
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
419
422
|
# Create new table
|
420
423
|
conn.execute(create_sql)
|
421
424
|
|
@@ -488,7 +491,7 @@ class ColumnManager:
|
|
488
491
|
|
489
492
|
# If making NOT NULL, check for NULL values
|
490
493
|
if not nullable:
|
491
|
-
with DatabaseConnection(self.db_path) as conn:
|
494
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
492
495
|
cursor = conn.execute(
|
493
496
|
f"SELECT COUNT(*) FROM {table_name} WHERE {column_name} IS NULL"
|
494
497
|
)
|
cinchdb/managers/data.py
CHANGED
@@ -9,7 +9,7 @@ from pydantic import BaseModel
|
|
9
9
|
|
10
10
|
from cinchdb.core.connection import DatabaseConnection
|
11
11
|
from cinchdb.core.path_utils import get_tenant_db_path
|
12
|
-
from cinchdb.core.
|
12
|
+
from cinchdb.core.maintenance_utils import check_maintenance_mode
|
13
13
|
from cinchdb.managers.table import TableManager
|
14
14
|
from cinchdb.managers.query import QueryManager
|
15
15
|
|
@@ -20,7 +20,8 @@ class DataManager:
|
|
20
20
|
"""Manages data operations within a database tenant."""
|
21
21
|
|
22
22
|
def __init__(
|
23
|
-
self, project_root: Path, database: str, branch: str, tenant: str = "main"
|
23
|
+
self, project_root: Path, database: str, branch: str, tenant: str = "main",
|
24
|
+
encryption_manager=None
|
24
25
|
):
|
25
26
|
"""Initialize data manager.
|
26
27
|
|
@@ -29,14 +30,16 @@ class DataManager:
|
|
29
30
|
database: Database name
|
30
31
|
branch: Branch name
|
31
32
|
tenant: Tenant name (default: main)
|
33
|
+
encryption_manager: EncryptionManager instance for encrypted connections
|
32
34
|
"""
|
33
35
|
self.project_root = Path(project_root)
|
34
36
|
self.database = database
|
35
37
|
self.branch = branch
|
36
38
|
self.tenant = tenant
|
39
|
+
self.encryption_manager = encryption_manager
|
37
40
|
self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
|
38
|
-
self.table_manager = TableManager(project_root, database, branch, tenant)
|
39
|
-
self.query_manager = QueryManager(project_root, database, branch, tenant)
|
41
|
+
self.table_manager = TableManager(project_root, database, branch, tenant, encryption_manager)
|
42
|
+
self.query_manager = QueryManager(project_root, database, branch, tenant, encryption_manager)
|
40
43
|
|
41
44
|
def select(
|
42
45
|
self,
|
@@ -73,7 +76,7 @@ class DataManager:
|
|
73
76
|
if offset:
|
74
77
|
query += f" OFFSET {offset}"
|
75
78
|
|
76
|
-
with DatabaseConnection(self.db_path) as conn:
|
79
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
77
80
|
cursor = conn.execute(query, params)
|
78
81
|
rows = cursor.fetchall()
|
79
82
|
|
@@ -130,7 +133,7 @@ class DataManager:
|
|
130
133
|
VALUES ({", ".join(placeholders)})
|
131
134
|
"""
|
132
135
|
|
133
|
-
with DatabaseConnection(self.db_path) as conn:
|
136
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
134
137
|
try:
|
135
138
|
conn.execute(query, record_data)
|
136
139
|
conn.commit()
|
@@ -233,7 +236,7 @@ class DataManager:
|
|
233
236
|
|
234
237
|
params = {**update_data, "id": data["id"]}
|
235
238
|
|
236
|
-
with DatabaseConnection(self.db_path) as conn:
|
239
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
237
240
|
try:
|
238
241
|
conn.execute(query, params)
|
239
242
|
conn.commit()
|
@@ -273,7 +276,7 @@ class DataManager:
|
|
273
276
|
|
274
277
|
query = f"DELETE FROM {table_name} WHERE {where_clause}"
|
275
278
|
|
276
|
-
with DatabaseConnection(self.db_path) as conn:
|
279
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
277
280
|
try:
|
278
281
|
cursor = conn.execute(query, params)
|
279
282
|
deleted_count = cursor.rowcount
|
@@ -320,7 +323,7 @@ class DataManager:
|
|
320
323
|
table_name = self._get_table_name(type(instances[0]))
|
321
324
|
created_instances = []
|
322
325
|
|
323
|
-
with DatabaseConnection(self.db_path) as conn:
|
326
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
324
327
|
try:
|
325
328
|
for instance in instances:
|
326
329
|
data = instance.model_dump()
|
@@ -370,7 +373,7 @@ class DataManager:
|
|
370
373
|
if where_clause:
|
371
374
|
query += f" WHERE {where_clause}"
|
372
375
|
|
373
|
-
with DatabaseConnection(self.db_path) as conn:
|
376
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
374
377
|
cursor = conn.execute(query, params)
|
375
378
|
result = cursor.fetchone()
|
376
379
|
return result["count"] if result else 0
|
@@ -521,7 +524,7 @@ class DataManager:
|
|
521
524
|
|
522
525
|
sql = f"DELETE FROM {table} WHERE {where_clause}"
|
523
526
|
|
524
|
-
with DatabaseConnection(self.db_path) as conn:
|
527
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
525
528
|
cursor = conn.execute(sql, params)
|
526
529
|
conn.commit()
|
527
530
|
return cursor.rowcount
|
@@ -585,7 +588,7 @@ class DataManager:
|
|
585
588
|
|
586
589
|
sql = f"UPDATE {table} SET {', '.join(set_clauses)} WHERE {where_clause}"
|
587
590
|
|
588
|
-
with DatabaseConnection(self.db_path) as conn:
|
591
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
589
592
|
cursor = conn.execute(sql, all_params)
|
590
593
|
conn.commit()
|
591
594
|
return cursor.rowcount
|
@@ -616,7 +619,7 @@ class DataManager:
|
|
616
619
|
sql = f"UPDATE {table} SET {set_clause} WHERE id = ?"
|
617
620
|
params.append(record_id)
|
618
621
|
|
619
|
-
with DatabaseConnection(self.db_path) as conn:
|
622
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
620
623
|
cursor = conn.execute(sql, params)
|
621
624
|
conn.commit()
|
622
625
|
|
@@ -645,7 +648,7 @@ class DataManager:
|
|
645
648
|
"""
|
646
649
|
sql = f"DELETE FROM {table} WHERE id = ?"
|
647
650
|
|
648
|
-
with DatabaseConnection(self.db_path) as conn:
|
651
|
+
with DatabaseConnection(self.db_path, tenant_id=self.tenant, encryption_manager=self.encryption_manager) as conn:
|
649
652
|
cursor = conn.execute(sql, [record_id])
|
650
653
|
conn.commit()
|
651
654
|
return cursor.rowcount > 0
|