cinchdb 0.1.15__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/config.py +4 -13
- cinchdb/core/connection.py +2 -1
- cinchdb/core/database.py +17 -5
- cinchdb/core/maintenance_utils.py +43 -0
- cinchdb/core/path_utils.py +19 -21
- cinchdb/core/tenant_activation.py +216 -0
- cinchdb/infrastructure/metadata_db.py +96 -3
- cinchdb/managers/change_applier.py +21 -22
- cinchdb/managers/column.py +1 -1
- cinchdb/managers/data.py +1 -1
- cinchdb/managers/table.py +1 -1
- cinchdb/managers/tenant.py +1 -1
- 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-0.1.15.dist-info → cinchdb-0.1.17.dist-info}/METADATA +5 -1
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.17.dist-info}/RECORD +22 -21
- cinchdb/core/maintenance.py +0 -73
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.17.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.17.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.15.dist-info → cinchdb-0.1.17.dist-info}/licenses/LICENSE +0 -0
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,8 +1,9 @@
|
|
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
|
|
cinchdb/core/database.py
CHANGED
@@ -1,11 +1,13 @@
|
|
1
1
|
"""Unified database connection interface for CinchDB."""
|
2
2
|
|
3
|
+
import os
|
3
4
|
from pathlib import Path
|
4
5
|
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
5
6
|
|
6
7
|
from cinchdb.models import Column, Change
|
7
8
|
from cinchdb.core.path_utils import get_project_root
|
8
9
|
from cinchdb.utils import validate_query_safe
|
10
|
+
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
9
11
|
|
10
12
|
if TYPE_CHECKING:
|
11
13
|
from cinchdb.managers.table import TableManager
|
@@ -78,7 +80,7 @@ class CinchDB:
|
|
78
80
|
self.database = database
|
79
81
|
self.branch = branch
|
80
82
|
self.tenant = tenant
|
81
|
-
|
83
|
+
|
82
84
|
# Determine connection type
|
83
85
|
if project_dir is not None:
|
84
86
|
# Local connection
|
@@ -120,16 +122,26 @@ class CinchDB:
|
|
120
122
|
return
|
121
123
|
|
122
124
|
# Check if this is a lazy database using metadata DB
|
123
|
-
|
124
|
-
|
125
|
-
with MetadataDB(self.project_dir) as metadata_db:
|
126
|
-
db_info = metadata_db.get_database(self.database)
|
125
|
+
metadata_db = get_metadata_db(self.project_dir)
|
126
|
+
db_info = metadata_db.get_database(self.database)
|
127
127
|
|
128
128
|
if db_info and not db_info['materialized']:
|
129
129
|
# Database exists in metadata but not materialized
|
130
130
|
from cinchdb.core.initializer import ProjectInitializer
|
131
131
|
initializer = ProjectInitializer(self.project_dir)
|
132
132
|
initializer.materialize_database(self.database)
|
133
|
+
|
134
|
+
def get_connection(self, db_path) -> "DatabaseConnection":
|
135
|
+
"""Get a database connection.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
db_path: Path to database file
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
DatabaseConnection instance
|
142
|
+
"""
|
143
|
+
from cinchdb.core.connection import DatabaseConnection
|
144
|
+
return DatabaseConnection(db_path)
|
133
145
|
|
134
146
|
@property
|
135
147
|
def session(self):
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Maintenance utilities for CinchDB operations."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
5
|
+
|
6
|
+
|
7
|
+
class MaintenanceError(Exception):
|
8
|
+
"""Exception raised when operation blocked by maintenance mode."""
|
9
|
+
pass
|
10
|
+
|
11
|
+
|
12
|
+
def check_maintenance_mode(project_root: Path, database: str, branch: str = None) -> None:
|
13
|
+
"""Check if database or branch is in maintenance mode and raise error if so.
|
14
|
+
|
15
|
+
Args:
|
16
|
+
project_root: Path to project root
|
17
|
+
database: Database name
|
18
|
+
branch: Branch name (optional)
|
19
|
+
|
20
|
+
Raises:
|
21
|
+
MaintenanceError: If database or branch is in maintenance mode
|
22
|
+
"""
|
23
|
+
try:
|
24
|
+
metadata_db = get_metadata_db(project_root)
|
25
|
+
|
26
|
+
# Check database-level maintenance
|
27
|
+
if metadata_db.is_database_in_maintenance(database):
|
28
|
+
info = metadata_db.get_maintenance_info(database)
|
29
|
+
reason = info.get("reason", "Database maintenance in progress") if info else "Database maintenance in progress"
|
30
|
+
raise MaintenanceError(f"Database '{database}' is in maintenance mode: {reason}")
|
31
|
+
|
32
|
+
# Check branch-level maintenance if branch specified
|
33
|
+
if branch and metadata_db.is_branch_in_maintenance(database, branch):
|
34
|
+
info = metadata_db.get_maintenance_info(database, branch)
|
35
|
+
reason = info.get("reason", "Branch maintenance in progress") if info else "Branch maintenance in progress"
|
36
|
+
raise MaintenanceError(f"Branch '{database}/{branch}' is in maintenance mode: {reason}")
|
37
|
+
|
38
|
+
except MaintenanceError:
|
39
|
+
raise # Re-raise maintenance errors
|
40
|
+
except Exception:
|
41
|
+
# If we can't check maintenance status, allow the operation to proceed
|
42
|
+
# This prevents maintenance check failures from blocking normal operations
|
43
|
+
pass
|
cinchdb/core/path_utils.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
4
|
from typing import List
|
5
|
+
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
5
6
|
|
6
7
|
|
7
8
|
def get_project_root(start_path: Path) -> Path:
|
@@ -119,10 +120,9 @@ def list_databases(project_root: Path) -> List[str]:
|
|
119
120
|
if not metadata_db_path.exists():
|
120
121
|
return []
|
121
122
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
return sorted(record['name'] for record in db_records)
|
123
|
+
metadata_db = get_metadata_db(project_root)
|
124
|
+
db_records = metadata_db.list_databases()
|
125
|
+
return sorted(record['name'] for record in db_records)
|
126
126
|
|
127
127
|
|
128
128
|
def list_branches(project_root: Path, database: str) -> List[str]:
|
@@ -139,13 +139,12 @@ def list_branches(project_root: Path, database: str) -> List[str]:
|
|
139
139
|
if not metadata_db_path.exists():
|
140
140
|
return []
|
141
141
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
return sorted(record['name'] for record in branch_records)
|
142
|
+
metadata_db = get_metadata_db(project_root)
|
143
|
+
db_info = metadata_db.get_database(database)
|
144
|
+
if not db_info:
|
145
|
+
return []
|
146
|
+
branch_records = metadata_db.list_branches(db_info['id'])
|
147
|
+
return sorted(record['name'] for record in branch_records)
|
149
148
|
|
150
149
|
|
151
150
|
def list_tenants(project_root: Path, database: str, branch: str) -> List[str]:
|
@@ -163,13 +162,12 @@ def list_tenants(project_root: Path, database: str, branch: str) -> List[str]:
|
|
163
162
|
if not metadata_db_path.exists():
|
164
163
|
return []
|
165
164
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
return sorted(record['name'] for record in tenant_records)
|
165
|
+
metadata_db = get_metadata_db(project_root)
|
166
|
+
db_info = metadata_db.get_database(database)
|
167
|
+
if not db_info:
|
168
|
+
return []
|
169
|
+
branch_info = metadata_db.get_branch(db_info['id'], branch)
|
170
|
+
if not branch_info:
|
171
|
+
return []
|
172
|
+
tenant_records = metadata_db.list_tenants(branch_info['id'])
|
173
|
+
return sorted(record['name'] for record in tenant_records)
|
@@ -0,0 +1,216 @@
|
|
1
|
+
"""Tenant activation and caching.
|
2
|
+
|
3
|
+
This module handles local caching of tenant databases with
|
4
|
+
LRU eviction and TTL management. WAL streaming is handled
|
5
|
+
at the api-server layer, not in the core library.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import sqlite3
|
10
|
+
import hashlib
|
11
|
+
from pathlib import Path
|
12
|
+
from typing import Optional, Dict, Any
|
13
|
+
from datetime import datetime, timedelta
|
14
|
+
|
15
|
+
from cinchdb.core.connection import DatabaseConnection
|
16
|
+
|
17
|
+
|
18
|
+
class TenantCache:
|
19
|
+
"""Manages local cache of activated tenant databases.
|
20
|
+
|
21
|
+
This implements an LRU cache with TTL for tenant SQLite files.
|
22
|
+
Tenants are activated on-demand from WAL replay and cached locally.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, cache_dir: Optional[Path] = None, max_size_gb: float = 10.0):
|
26
|
+
"""Initialize tenant cache.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
cache_dir: Directory for cached tenant databases
|
30
|
+
max_size_gb: Maximum cache size in GB before eviction
|
31
|
+
"""
|
32
|
+
self.cache_dir = cache_dir or Path("/var/cache/cinchdb/tenants")
|
33
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
34
|
+
self.max_size_bytes = int(max_size_gb * 1024 * 1024 * 1024)
|
35
|
+
|
36
|
+
# Track cached tenants: tenant_key -> (path, version, last_access)
|
37
|
+
self.cache_index: Dict[str, Dict[str, Any]] = {}
|
38
|
+
|
39
|
+
def _get_cache_path(self, database: str, branch: str, tenant: str) -> Path:
|
40
|
+
"""Get cache path for a tenant database.
|
41
|
+
|
42
|
+
Uses sharding to avoid too many files in one directory.
|
43
|
+
"""
|
44
|
+
# Create tenant key
|
45
|
+
tenant_key = f"{database}_{branch}_{tenant}"
|
46
|
+
|
47
|
+
# Calculate shard (2-level sharding like production)
|
48
|
+
hash_val = hashlib.sha256(tenant_key.encode()).hexdigest()
|
49
|
+
shard1 = hash_val[:2]
|
50
|
+
shard2 = hash_val[2:4]
|
51
|
+
|
52
|
+
# Build path
|
53
|
+
cache_path = self.cache_dir / database / branch / shard1 / shard2
|
54
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
55
|
+
|
56
|
+
return cache_path / f"{tenant}.db"
|
57
|
+
|
58
|
+
def get_tenant_connection(
|
59
|
+
self,
|
60
|
+
database: str,
|
61
|
+
branch: str,
|
62
|
+
tenant: str,
|
63
|
+
wal_capture=None
|
64
|
+
) -> DatabaseConnection:
|
65
|
+
"""Get a database connection for a tenant.
|
66
|
+
|
67
|
+
If the tenant is not cached, activates it from WAL replay.
|
68
|
+
|
69
|
+
Args:
|
70
|
+
database: Database name
|
71
|
+
branch: Branch name
|
72
|
+
tenant: Tenant name
|
73
|
+
wal_capture: Optional WAL capture for streaming
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
DatabaseConnection to the tenant database
|
77
|
+
"""
|
78
|
+
tenant_key = f"{database}_{branch}_{tenant}"
|
79
|
+
cache_path = self._get_cache_path(database, branch, tenant)
|
80
|
+
|
81
|
+
# Check if already cached
|
82
|
+
if cache_path.exists():
|
83
|
+
# Update last access time
|
84
|
+
if tenant_key in self.cache_index:
|
85
|
+
self.cache_index[tenant_key]["last_access"] = datetime.now()
|
86
|
+
|
87
|
+
return DatabaseConnection(cache_path, wal_capture=wal_capture)
|
88
|
+
|
89
|
+
# Not cached - create empty database
|
90
|
+
# WAL streaming/activation happens at api-server layer if enabled
|
91
|
+
return self._create_empty_database(cache_path, wal_capture)
|
92
|
+
|
93
|
+
def _create_empty_database(
|
94
|
+
self,
|
95
|
+
cache_path: Path,
|
96
|
+
wal_capture=None
|
97
|
+
) -> DatabaseConnection:
|
98
|
+
"""Create an empty database (for lazy tenants or non-streaming mode).
|
99
|
+
|
100
|
+
Args:
|
101
|
+
cache_path: Path where database should be created
|
102
|
+
wal_capture: Optional WAL capture
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
DatabaseConnection to the new database
|
106
|
+
"""
|
107
|
+
# Create empty SQLite database
|
108
|
+
conn = sqlite3.connect(str(cache_path))
|
109
|
+
|
110
|
+
# Set up WAL mode
|
111
|
+
conn.execute("PRAGMA journal_mode = WAL")
|
112
|
+
conn.execute("PRAGMA synchronous = NORMAL")
|
113
|
+
conn.commit()
|
114
|
+
conn.close()
|
115
|
+
|
116
|
+
return DatabaseConnection(cache_path, wal_capture=wal_capture)
|
117
|
+
|
118
|
+
|
119
|
+
def _evict_if_needed(self):
|
120
|
+
"""Evict least recently used tenants if cache is too large."""
|
121
|
+
# Calculate total cache size
|
122
|
+
total_size = sum(
|
123
|
+
info["size_bytes"] for info in self.cache_index.values()
|
124
|
+
)
|
125
|
+
|
126
|
+
if total_size <= self.max_size_bytes:
|
127
|
+
return # Cache is within limits
|
128
|
+
|
129
|
+
# Sort by last access time (LRU)
|
130
|
+
sorted_tenants = sorted(
|
131
|
+
self.cache_index.items(),
|
132
|
+
key=lambda x: x[1]["last_access"]
|
133
|
+
)
|
134
|
+
|
135
|
+
# Evict until under limit
|
136
|
+
for tenant_key, info in sorted_tenants:
|
137
|
+
if total_size <= self.max_size_bytes:
|
138
|
+
break
|
139
|
+
|
140
|
+
# Delete cached database
|
141
|
+
cache_path = info["path"]
|
142
|
+
if cache_path.exists():
|
143
|
+
# Delete SQLite files (main, WAL, SHM)
|
144
|
+
for suffix in ["", "-wal", "-shm"]:
|
145
|
+
file_path = Path(str(cache_path) + suffix)
|
146
|
+
if file_path.exists():
|
147
|
+
file_path.unlink()
|
148
|
+
|
149
|
+
print(f"Evicted cached tenant: {tenant_key}")
|
150
|
+
|
151
|
+
# Remove from index
|
152
|
+
total_size -= info["size_bytes"]
|
153
|
+
del self.cache_index[tenant_key]
|
154
|
+
|
155
|
+
def invalidate_tenant(self, database: str, branch: str, tenant: str):
|
156
|
+
"""Invalidate a cached tenant (force re-activation on next access).
|
157
|
+
|
158
|
+
Args:
|
159
|
+
database: Database name
|
160
|
+
branch: Branch name
|
161
|
+
tenant: Tenant name
|
162
|
+
"""
|
163
|
+
tenant_key = f"{database}_{branch}_{tenant}"
|
164
|
+
|
165
|
+
if tenant_key in self.cache_index:
|
166
|
+
info = self.cache_index[tenant_key]
|
167
|
+
cache_path = info["path"]
|
168
|
+
|
169
|
+
# Delete cached files
|
170
|
+
if cache_path.exists():
|
171
|
+
for suffix in ["", "-wal", "-shm"]:
|
172
|
+
file_path = Path(str(cache_path) + suffix)
|
173
|
+
if file_path.exists():
|
174
|
+
file_path.unlink()
|
175
|
+
|
176
|
+
# Remove from index
|
177
|
+
del self.cache_index[tenant_key]
|
178
|
+
print(f"Invalidated cached tenant: {tenant_key}")
|
179
|
+
|
180
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
181
|
+
"""Get cache statistics.
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
Dictionary with cache stats
|
185
|
+
"""
|
186
|
+
total_size = sum(
|
187
|
+
info["size_bytes"] for info in self.cache_index.values()
|
188
|
+
)
|
189
|
+
|
190
|
+
return {
|
191
|
+
"cached_tenants": len(self.cache_index),
|
192
|
+
"total_size_bytes": total_size,
|
193
|
+
"total_size_mb": round(total_size / (1024 * 1024), 2),
|
194
|
+
"max_size_gb": self.max_size_bytes / (1024 * 1024 * 1024),
|
195
|
+
"cache_dir": str(self.cache_dir)
|
196
|
+
}
|
197
|
+
|
198
|
+
|
199
|
+
# Global cache instance (singleton)
|
200
|
+
_tenant_cache: Optional[TenantCache] = None
|
201
|
+
|
202
|
+
|
203
|
+
def get_tenant_cache() -> TenantCache:
|
204
|
+
"""Get the global tenant cache instance.
|
205
|
+
|
206
|
+
Returns:
|
207
|
+
TenantCache singleton
|
208
|
+
"""
|
209
|
+
global _tenant_cache
|
210
|
+
|
211
|
+
if _tenant_cache is None:
|
212
|
+
cache_dir = os.getenv("TENANT_CACHE_DIR", "/var/cache/cinchdb")
|
213
|
+
cache_size_gb = float(os.getenv("TENANT_CACHE_SIZE_GB", "10.0"))
|
214
|
+
_tenant_cache = TenantCache(Path(cache_dir), cache_size_gb)
|
215
|
+
|
216
|
+
return _tenant_cache
|
@@ -37,6 +37,7 @@ class MetadataDB:
|
|
37
37
|
self.conn.execute("PRAGMA foreign_keys = ON")
|
38
38
|
self.conn.execute("PRAGMA journal_mode = WAL") # Better concurrency
|
39
39
|
|
40
|
+
|
40
41
|
def _create_tables(self):
|
41
42
|
"""Create the metadata tables if they don't exist."""
|
42
43
|
with self.conn:
|
@@ -47,6 +48,9 @@ class MetadataDB:
|
|
47
48
|
name TEXT NOT NULL UNIQUE,
|
48
49
|
description TEXT,
|
49
50
|
materialized BOOLEAN DEFAULT FALSE,
|
51
|
+
maintenance_mode BOOLEAN DEFAULT FALSE,
|
52
|
+
maintenance_reason TEXT,
|
53
|
+
maintenance_started_at TIMESTAMP,
|
50
54
|
metadata JSON,
|
51
55
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
52
56
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
@@ -62,6 +66,9 @@ class MetadataDB:
|
|
62
66
|
parent_branch TEXT,
|
63
67
|
schema_version TEXT,
|
64
68
|
materialized BOOLEAN DEFAULT FALSE,
|
69
|
+
maintenance_mode BOOLEAN DEFAULT FALSE,
|
70
|
+
maintenance_reason TEXT,
|
71
|
+
maintenance_started_at TIMESTAMP,
|
65
72
|
metadata JSON,
|
66
73
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
67
74
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
@@ -116,9 +123,7 @@ class MetadataDB:
|
|
116
123
|
CREATE INDEX IF NOT EXISTS idx_tenants_shard
|
117
124
|
ON tenants(shard)
|
118
125
|
""")
|
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
|
126
|
+
|
122
127
|
try:
|
123
128
|
self.conn.execute("""
|
124
129
|
ALTER TABLE branches ADD COLUMN cdc_enabled BOOLEAN DEFAULT FALSE
|
@@ -376,6 +381,94 @@ class MetadataDB:
|
|
376
381
|
|
377
382
|
return len(tenants)
|
378
383
|
|
384
|
+
# Maintenance Mode Methods
|
385
|
+
|
386
|
+
def set_database_maintenance(self, database_name: str, enabled: bool, reason: Optional[str] = None) -> None:
|
387
|
+
"""Set maintenance mode for a database."""
|
388
|
+
with self.conn:
|
389
|
+
if enabled:
|
390
|
+
self.conn.execute("""
|
391
|
+
UPDATE databases
|
392
|
+
SET maintenance_mode = TRUE,
|
393
|
+
maintenance_reason = ?,
|
394
|
+
maintenance_started_at = CURRENT_TIMESTAMP
|
395
|
+
WHERE name = ?
|
396
|
+
""", (reason, database_name))
|
397
|
+
else:
|
398
|
+
self.conn.execute("""
|
399
|
+
UPDATE databases
|
400
|
+
SET maintenance_mode = FALSE,
|
401
|
+
maintenance_reason = NULL,
|
402
|
+
maintenance_started_at = NULL
|
403
|
+
WHERE name = ?
|
404
|
+
""", (database_name,))
|
405
|
+
|
406
|
+
def set_branch_maintenance(self, database_name: str, branch_name: str, enabled: bool, reason: Optional[str] = None) -> None:
|
407
|
+
"""Set maintenance mode for a branch."""
|
408
|
+
with self.conn:
|
409
|
+
if enabled:
|
410
|
+
self.conn.execute("""
|
411
|
+
UPDATE branches
|
412
|
+
SET maintenance_mode = TRUE,
|
413
|
+
maintenance_reason = ?,
|
414
|
+
maintenance_started_at = CURRENT_TIMESTAMP
|
415
|
+
WHERE database_id = (SELECT id FROM databases WHERE name = ?)
|
416
|
+
AND name = ?
|
417
|
+
""", (reason, database_name, branch_name))
|
418
|
+
else:
|
419
|
+
self.conn.execute("""
|
420
|
+
UPDATE branches
|
421
|
+
SET maintenance_mode = FALSE,
|
422
|
+
maintenance_reason = NULL,
|
423
|
+
maintenance_started_at = NULL
|
424
|
+
WHERE database_id = (SELECT id FROM databases WHERE name = ?)
|
425
|
+
AND name = ?
|
426
|
+
""", (database_name, branch_name))
|
427
|
+
|
428
|
+
def is_database_in_maintenance(self, database_name: str) -> bool:
|
429
|
+
"""Check if database is in maintenance mode."""
|
430
|
+
cursor = self.conn.execute("""
|
431
|
+
SELECT maintenance_mode FROM databases WHERE name = ?
|
432
|
+
""", (database_name,))
|
433
|
+
row = cursor.fetchone()
|
434
|
+
return bool(row and row['maintenance_mode'])
|
435
|
+
|
436
|
+
def is_branch_in_maintenance(self, database_name: str, branch_name: str) -> bool:
|
437
|
+
"""Check if branch is in maintenance mode."""
|
438
|
+
cursor = self.conn.execute("""
|
439
|
+
SELECT maintenance_mode FROM branches
|
440
|
+
WHERE database_id = (SELECT id FROM databases WHERE name = ?)
|
441
|
+
AND name = ?
|
442
|
+
""", (database_name, branch_name))
|
443
|
+
row = cursor.fetchone()
|
444
|
+
return bool(row and row['maintenance_mode'])
|
445
|
+
|
446
|
+
def get_maintenance_info(self, database_name: str, branch_name: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
447
|
+
"""Get maintenance mode information."""
|
448
|
+
if branch_name:
|
449
|
+
# Check branch maintenance
|
450
|
+
cursor = self.conn.execute("""
|
451
|
+
SELECT maintenance_mode, maintenance_reason, maintenance_started_at
|
452
|
+
FROM branches
|
453
|
+
WHERE database_id = (SELECT id FROM databases WHERE name = ?)
|
454
|
+
AND name = ?
|
455
|
+
""", (database_name, branch_name))
|
456
|
+
else:
|
457
|
+
# Check database maintenance
|
458
|
+
cursor = self.conn.execute("""
|
459
|
+
SELECT maintenance_mode, maintenance_reason, maintenance_started_at
|
460
|
+
FROM databases WHERE name = ?
|
461
|
+
""", (database_name,))
|
462
|
+
|
463
|
+
row = cursor.fetchone()
|
464
|
+
if row and row['maintenance_mode']:
|
465
|
+
return {
|
466
|
+
'enabled': True,
|
467
|
+
'reason': row['maintenance_reason'],
|
468
|
+
'started_at': row['maintenance_started_at']
|
469
|
+
}
|
470
|
+
return None
|
471
|
+
|
379
472
|
def close(self):
|
380
473
|
"""Close the database connection."""
|
381
474
|
if self.conn:
|
@@ -11,6 +11,7 @@ from cinchdb.managers.change_tracker import ChangeTracker
|
|
11
11
|
from cinchdb.managers.tenant import TenantManager
|
12
12
|
from cinchdb.core.connection import DatabaseConnection
|
13
13
|
from cinchdb.core.path_utils import 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/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
|
|
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
|
|
cinchdb/managers/table.py
CHANGED
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|
9
9
|
from cinchdb.models import Index
|
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.change_tracker import ChangeTracker
|
14
14
|
from cinchdb.managers.tenant import TenantManager
|
15
15
|
|
cinchdb/managers/tenant.py
CHANGED
@@ -15,7 +15,7 @@ from cinchdb.core.path_utils import (
|
|
15
15
|
list_tenants,
|
16
16
|
)
|
17
17
|
from cinchdb.core.connection import DatabaseConnection
|
18
|
-
from cinchdb.core.
|
18
|
+
from cinchdb.core.maintenance_utils import check_maintenance_mode
|
19
19
|
from cinchdb.utils.name_validator import validate_name
|
20
20
|
from cinchdb.infrastructure.metadata_db import MetadataDB
|
21
21
|
from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
|
cinchdb/managers/view.py
CHANGED
@@ -6,7 +6,7 @@ from typing import List
|
|
6
6
|
from cinchdb.models import View, 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
|
|
12
12
|
|