cinchdb 0.1.2__py3-none-any.whl → 0.1.4__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/branch.py +22 -13
- cinchdb/cli/commands/column.py +27 -14
- cinchdb/cli/commands/database.py +2 -2
- cinchdb/cli/commands/query.py +19 -12
- cinchdb/cli/commands/remote.py +32 -28
- cinchdb/cli/commands/table.py +20 -16
- cinchdb/cli/commands/tenant.py +4 -4
- cinchdb/cli/main.py +21 -11
- cinchdb/cli/utils.py +8 -6
- cinchdb/config.py +18 -45
- cinchdb/core/__init__.py +2 -1
- cinchdb/core/database.py +9 -5
- cinchdb/core/initializer.py +214 -0
- cinchdb/managers/branch.py +1 -3
- cinchdb/managers/column.py +13 -9
- cinchdb/managers/query.py +12 -6
- cinchdb/managers/table.py +12 -9
- cinchdb/managers/tenant.py +3 -3
- cinchdb/models/branch.py +1 -1
- cinchdb/models/database.py +1 -1
- cinchdb/models/table.py +5 -8
- cinchdb/models/tenant.py +1 -1
- cinchdb/utils/__init__.py +5 -5
- cinchdb/utils/name_validator.py +62 -31
- cinchdb/utils/sql_validator.py +91 -41
- {cinchdb-0.1.2.dist-info → cinchdb-0.1.4.dist-info}/METADATA +2 -2
- cinchdb-0.1.4.dist-info/RECORD +54 -0
- cinchdb-0.1.2.dist-info/RECORD +0 -53
- {cinchdb-0.1.2.dist-info → cinchdb-0.1.4.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.2.dist-info → cinchdb-0.1.4.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.2.dist-info → cinchdb-0.1.4.dist-info}/licenses/LICENSE +0 -0
cinchdb/config.py
CHANGED
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, ConfigDict
|
|
9
9
|
|
10
10
|
class RemoteConfig(BaseModel):
|
11
11
|
"""Configuration for a remote CinchDB instance."""
|
12
|
-
|
12
|
+
|
13
13
|
url: str = Field(description="Base URL of the remote CinchDB API")
|
14
14
|
key: str = Field(description="API key for authentication")
|
15
15
|
|
@@ -50,7 +50,7 @@ class Config:
|
|
50
50
|
env_dir = os.environ.get("CINCHDB_PROJECT_DIR")
|
51
51
|
if env_dir:
|
52
52
|
project_dir = Path(env_dir)
|
53
|
-
|
53
|
+
|
54
54
|
self.project_dir = Path(project_dir) if project_dir else Path.cwd()
|
55
55
|
self.config_dir = self.project_dir / ".cinchdb"
|
56
56
|
self.config_path = self.config_dir / "config.toml"
|
@@ -86,24 +86,24 @@ class Config:
|
|
86
86
|
# Override database and branch
|
87
87
|
if env_db := os.environ.get("CINCHDB_DATABASE"):
|
88
88
|
data["active_database"] = env_db
|
89
|
-
|
89
|
+
|
90
90
|
if env_branch := os.environ.get("CINCHDB_BRANCH"):
|
91
91
|
data["active_branch"] = env_branch
|
92
|
-
|
92
|
+
|
93
93
|
# Override or create remote configuration
|
94
94
|
env_url = os.environ.get("CINCHDB_REMOTE_URL")
|
95
95
|
env_key = os.environ.get("CINCHDB_API_KEY")
|
96
|
-
|
96
|
+
|
97
97
|
if env_url and env_key:
|
98
98
|
# Create or update "env" remote
|
99
99
|
if "remotes" not in data:
|
100
100
|
data["remotes"] = {}
|
101
|
-
|
101
|
+
|
102
102
|
data["remotes"]["env"] = {
|
103
103
|
"url": env_url.rstrip("/"), # Remove trailing slash
|
104
|
-
"key": env_key
|
104
|
+
"key": env_key,
|
105
105
|
}
|
106
|
-
|
106
|
+
|
107
107
|
# Make it active if no other remote is set
|
108
108
|
if not data.get("active_remote"):
|
109
109
|
data["active_remote"] = "env"
|
@@ -130,48 +130,21 @@ class Config:
|
|
130
130
|
for alias, remote in config_dict["remotes"].items():
|
131
131
|
if isinstance(remote, dict):
|
132
132
|
config_dict["remotes"][alias] = remote
|
133
|
-
|
133
|
+
|
134
134
|
with open(self.config_path, "w") as f:
|
135
135
|
toml.dump(config_dict, f)
|
136
136
|
|
137
137
|
def init_project(self) -> ProjectConfig:
|
138
|
-
"""Initialize a new CinchDB project with default configuration.
|
139
|
-
if self.exists:
|
140
|
-
raise FileExistsError(f"Project already exists at {self.config_dir}")
|
138
|
+
"""Initialize a new CinchDB project with default configuration.
|
141
139
|
|
142
|
-
|
143
|
-
|
144
|
-
|
140
|
+
This method now delegates to the ProjectInitializer for the actual
|
141
|
+
initialization logic.
|
142
|
+
"""
|
143
|
+
from cinchdb.core.initializer import ProjectInitializer
|
145
144
|
|
146
|
-
|
147
|
-
|
145
|
+
initializer = ProjectInitializer(self.project_dir)
|
146
|
+
config = initializer.init_project()
|
148
147
|
|
148
|
+
# Load the config into this instance
|
149
|
+
self._config = config
|
149
150
|
return config
|
150
|
-
|
151
|
-
def _create_default_structure(self) -> None:
|
152
|
-
"""Create default project structure."""
|
153
|
-
# Create main database with main branch
|
154
|
-
db_path = self.config_dir / "databases" / "main" / "branches" / "main"
|
155
|
-
db_path.mkdir(parents=True, exist_ok=True)
|
156
|
-
|
157
|
-
# Create metadata files
|
158
|
-
from datetime import datetime, timezone
|
159
|
-
|
160
|
-
metadata = {"created_at": datetime.now(timezone.utc).isoformat()}
|
161
|
-
|
162
|
-
import json
|
163
|
-
|
164
|
-
with open(db_path / "metadata.json", "w") as f:
|
165
|
-
json.dump(metadata, f, indent=2)
|
166
|
-
|
167
|
-
# Create empty changes file
|
168
|
-
with open(db_path / "changes.json", "w") as f:
|
169
|
-
json.dump([], f, indent=2)
|
170
|
-
|
171
|
-
# Create main tenant directory and database
|
172
|
-
tenant_dir = db_path / "tenants"
|
173
|
-
tenant_dir.mkdir(exist_ok=True)
|
174
|
-
|
175
|
-
# Create main tenant database file
|
176
|
-
main_db = tenant_dir / "main.db"
|
177
|
-
main_db.touch()
|
cinchdb/core/__init__.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Core CinchDB functionality."""
|
2
2
|
|
3
3
|
from cinchdb.core.database import CinchDB, connect, connect_api
|
4
|
+
from cinchdb.core.initializer import init_project, init_database
|
4
5
|
|
5
|
-
__all__ = ["CinchDB", "connect", "connect_api"]
|
6
|
+
__all__ = ["CinchDB", "connect", "connect_api", "init_project", "init_database"]
|
cinchdb/core/database.py
CHANGED
@@ -3,10 +3,9 @@
|
|
3
3
|
from pathlib import Path
|
4
4
|
from typing import List, Dict, Any, Optional, TYPE_CHECKING
|
5
5
|
|
6
|
-
from cinchdb.config import Config
|
7
6
|
from cinchdb.models import Column, Change
|
8
7
|
from cinchdb.core.path_utils import get_project_root
|
9
|
-
from cinchdb.utils import validate_query_safe
|
8
|
+
from cinchdb.utils import validate_query_safe
|
10
9
|
|
11
10
|
if TYPE_CHECKING:
|
12
11
|
from cinchdb.managers.table import TableManager
|
@@ -307,7 +306,10 @@ class CinchDB:
|
|
307
306
|
# Convenience methods for common operations
|
308
307
|
|
309
308
|
def query(
|
310
|
-
self,
|
309
|
+
self,
|
310
|
+
sql: str,
|
311
|
+
params: Optional[List[Any]] = None,
|
312
|
+
skip_validation: bool = False,
|
311
313
|
) -> List[Dict[str, Any]]:
|
312
314
|
"""Execute a SQL query.
|
313
315
|
|
@@ -318,14 +320,14 @@ class CinchDB:
|
|
318
320
|
|
319
321
|
Returns:
|
320
322
|
List of result rows as dictionaries
|
321
|
-
|
323
|
+
|
322
324
|
Raises:
|
323
325
|
SQLValidationError: If the query contains restricted operations
|
324
326
|
"""
|
325
327
|
# Validate query unless explicitly skipped
|
326
328
|
if not skip_validation:
|
327
329
|
validate_query_safe(sql)
|
328
|
-
|
330
|
+
|
329
331
|
if self.is_local:
|
330
332
|
if self._query_manager is None:
|
331
333
|
from cinchdb.managers.query import QueryManager
|
@@ -427,6 +429,7 @@ class CinchDB:
|
|
427
429
|
"""
|
428
430
|
if self.is_local:
|
429
431
|
from cinchdb.managers.change_tracker import ChangeTracker
|
432
|
+
|
430
433
|
tracker = ChangeTracker(self.project_dir, self.database, self.branch)
|
431
434
|
return tracker.get_changes()
|
432
435
|
else:
|
@@ -435,6 +438,7 @@ class CinchDB:
|
|
435
438
|
# Convert API response to Change objects
|
436
439
|
from cinchdb.models import Change
|
437
440
|
from datetime import datetime
|
441
|
+
|
438
442
|
changes = []
|
439
443
|
for data in result.get("changes", []):
|
440
444
|
# Convert string dates back to datetime if present
|
@@ -0,0 +1,214 @@
|
|
1
|
+
"""Project initialization for CinchDB."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from datetime import datetime, timezone
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
from cinchdb.core.connection import DatabaseConnection
|
9
|
+
from cinchdb.config import ProjectConfig
|
10
|
+
|
11
|
+
|
12
|
+
class ProjectInitializer:
|
13
|
+
"""Handles initialization of CinchDB projects."""
|
14
|
+
|
15
|
+
def __init__(self, project_dir: Optional[Path] = None):
|
16
|
+
"""Initialize the project initializer.
|
17
|
+
|
18
|
+
Args:
|
19
|
+
project_dir: Path to project directory. If None, uses current directory.
|
20
|
+
"""
|
21
|
+
self.project_dir = Path(project_dir) if project_dir else Path.cwd()
|
22
|
+
self.config_dir = self.project_dir / ".cinchdb"
|
23
|
+
self.config_path = self.config_dir / "config.toml"
|
24
|
+
|
25
|
+
def init_project(
|
26
|
+
self, database_name: str = "main", branch_name: str = "main"
|
27
|
+
) -> ProjectConfig:
|
28
|
+
"""Initialize a new CinchDB project with default configuration.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
database_name: Name for the initial database (default: "main")
|
32
|
+
branch_name: Name for the initial branch (default: "main")
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
The created ProjectConfig
|
36
|
+
|
37
|
+
Raises:
|
38
|
+
FileExistsError: If project already exists at the location
|
39
|
+
"""
|
40
|
+
if self.config_path.exists():
|
41
|
+
raise FileExistsError(f"Project already exists at {self.config_dir}")
|
42
|
+
|
43
|
+
# Create config directory
|
44
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
45
|
+
|
46
|
+
# Create default config
|
47
|
+
config = ProjectConfig(active_database=database_name, active_branch=branch_name)
|
48
|
+
|
49
|
+
# Save config
|
50
|
+
self._save_config(config)
|
51
|
+
|
52
|
+
# Create default database structure
|
53
|
+
self._create_database_structure(database_name, branch_name)
|
54
|
+
|
55
|
+
return config
|
56
|
+
|
57
|
+
def init_database(
|
58
|
+
self,
|
59
|
+
database_name: str,
|
60
|
+
branch_name: str = "main",
|
61
|
+
description: Optional[str] = None,
|
62
|
+
) -> None:
|
63
|
+
"""Initialize a new database within an existing project.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
database_name: Name for the database
|
67
|
+
branch_name: Initial branch name (default: "main")
|
68
|
+
description: Optional description for the database
|
69
|
+
|
70
|
+
Raises:
|
71
|
+
FileNotFoundError: If project doesn't exist
|
72
|
+
FileExistsError: If database already exists
|
73
|
+
"""
|
74
|
+
if not self.config_path.exists():
|
75
|
+
raise FileNotFoundError(f"No CinchDB project found at {self.config_dir}")
|
76
|
+
|
77
|
+
db_path = self.config_dir / "databases" / database_name
|
78
|
+
if db_path.exists():
|
79
|
+
raise FileExistsError(f"Database '{database_name}' already exists")
|
80
|
+
|
81
|
+
# Create database structure
|
82
|
+
self._create_database_structure(database_name, branch_name, description)
|
83
|
+
|
84
|
+
def _create_database_structure(
|
85
|
+
self,
|
86
|
+
database_name: str,
|
87
|
+
branch_name: str = "main",
|
88
|
+
description: Optional[str] = None,
|
89
|
+
) -> None:
|
90
|
+
"""Create the directory structure for a database.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
database_name: Name of the database
|
94
|
+
branch_name: Name of the initial branch
|
95
|
+
description: Optional description
|
96
|
+
"""
|
97
|
+
# Create database branch path
|
98
|
+
branch_path = (
|
99
|
+
self.config_dir / "databases" / database_name / "branches" / branch_name
|
100
|
+
)
|
101
|
+
branch_path.mkdir(parents=True, exist_ok=True)
|
102
|
+
|
103
|
+
# Create metadata file
|
104
|
+
metadata = {
|
105
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
106
|
+
"name": branch_name,
|
107
|
+
"parent": None,
|
108
|
+
"description": description,
|
109
|
+
}
|
110
|
+
|
111
|
+
with open(branch_path / "metadata.json", "w") as f:
|
112
|
+
json.dump(metadata, f, indent=2)
|
113
|
+
|
114
|
+
# Create empty changes file
|
115
|
+
with open(branch_path / "changes.json", "w") as f:
|
116
|
+
json.dump([], f, indent=2)
|
117
|
+
|
118
|
+
# Create tenants directory
|
119
|
+
tenant_dir = branch_path / "tenants"
|
120
|
+
tenant_dir.mkdir(exist_ok=True)
|
121
|
+
|
122
|
+
# Create and initialize main tenant database
|
123
|
+
self._init_tenant_database(tenant_dir / "main.db")
|
124
|
+
|
125
|
+
def _init_tenant_database(self, db_path: Path) -> None:
|
126
|
+
"""Initialize a tenant database with proper PRAGMAs.
|
127
|
+
|
128
|
+
Args:
|
129
|
+
db_path: Path to the database file
|
130
|
+
"""
|
131
|
+
# Create the database file
|
132
|
+
db_path.touch()
|
133
|
+
|
134
|
+
# Initialize with proper PRAGMAs
|
135
|
+
with DatabaseConnection(db_path):
|
136
|
+
# The connection automatically sets up PRAGMAs in _connect():
|
137
|
+
# - journal_mode = WAL
|
138
|
+
# - synchronous = NORMAL
|
139
|
+
# - wal_autocheckpoint = 0
|
140
|
+
# - foreign_keys = ON
|
141
|
+
pass
|
142
|
+
|
143
|
+
def _save_config(self, config: ProjectConfig) -> None:
|
144
|
+
"""Save configuration to disk.
|
145
|
+
|
146
|
+
Args:
|
147
|
+
config: Configuration to save
|
148
|
+
"""
|
149
|
+
import toml
|
150
|
+
|
151
|
+
# Ensure config directory exists
|
152
|
+
self.config_dir.mkdir(parents=True, exist_ok=True)
|
153
|
+
|
154
|
+
# Convert to dict for TOML serialization
|
155
|
+
config_dict = config.model_dump()
|
156
|
+
|
157
|
+
# Convert RemoteConfig objects to dicts if present
|
158
|
+
if "remotes" in config_dict:
|
159
|
+
for alias, remote in config_dict["remotes"].items():
|
160
|
+
if isinstance(remote, dict):
|
161
|
+
config_dict["remotes"][alias] = remote
|
162
|
+
|
163
|
+
with open(self.config_path, "w") as f:
|
164
|
+
toml.dump(config_dict, f)
|
165
|
+
|
166
|
+
|
167
|
+
def init_project(
|
168
|
+
project_dir: Optional[Path] = None,
|
169
|
+
database_name: str = "main",
|
170
|
+
branch_name: str = "main",
|
171
|
+
) -> ProjectConfig:
|
172
|
+
"""Initialize a new CinchDB project.
|
173
|
+
|
174
|
+
This is a convenience function that creates a ProjectInitializer
|
175
|
+
and initializes a project.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
project_dir: Directory to initialize in (default: current directory)
|
179
|
+
database_name: Name for initial database (default: "main")
|
180
|
+
branch_name: Name for initial branch (default: "main")
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
The created ProjectConfig
|
184
|
+
|
185
|
+
Raises:
|
186
|
+
FileExistsError: If project already exists
|
187
|
+
"""
|
188
|
+
initializer = ProjectInitializer(project_dir)
|
189
|
+
return initializer.init_project(database_name, branch_name)
|
190
|
+
|
191
|
+
|
192
|
+
def init_database(
|
193
|
+
project_dir: Optional[Path] = None,
|
194
|
+
database_name: str = "main",
|
195
|
+
branch_name: str = "main",
|
196
|
+
description: Optional[str] = None,
|
197
|
+
) -> None:
|
198
|
+
"""Initialize a new database within an existing project.
|
199
|
+
|
200
|
+
This is a convenience function that creates a ProjectInitializer
|
201
|
+
and initializes a database.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
project_dir: Project directory (default: current directory)
|
205
|
+
database_name: Name for the database
|
206
|
+
branch_name: Initial branch name (default: "main")
|
207
|
+
description: Optional description
|
208
|
+
|
209
|
+
Raises:
|
210
|
+
FileNotFoundError: If project doesn't exist
|
211
|
+
FileExistsError: If database already exists
|
212
|
+
"""
|
213
|
+
initializer = ProjectInitializer(project_dir)
|
214
|
+
initializer.init_database(database_name, branch_name, description)
|
cinchdb/managers/branch.py
CHANGED
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
6
6
|
from typing import List, Dict, Any
|
7
7
|
from datetime import datetime, timezone
|
8
8
|
|
9
|
-
from cinchdb.config import Config
|
10
9
|
from cinchdb.models import Branch
|
11
10
|
from cinchdb.core.path_utils import (
|
12
11
|
get_database_path,
|
@@ -68,7 +67,7 @@ class BranchManager:
|
|
68
67
|
"""
|
69
68
|
# Validate new branch name
|
70
69
|
validate_name(new_branch_name, "branch")
|
71
|
-
|
70
|
+
|
72
71
|
# Validate source branch exists
|
73
72
|
if source_branch not in list_branches(self.project_root, self.database):
|
74
73
|
raise ValueError(f"Source branch '{source_branch}' does not exist")
|
@@ -122,7 +121,6 @@ class BranchManager:
|
|
122
121
|
branch_path = get_branch_path(self.project_root, self.database, branch_name)
|
123
122
|
shutil.rmtree(branch_path)
|
124
123
|
|
125
|
-
|
126
124
|
def get_branch_metadata(self, branch_name: str) -> Dict[str, Any]:
|
127
125
|
"""Get metadata for a branch.
|
128
126
|
|
cinchdb/managers/column.py
CHANGED
@@ -439,16 +439,20 @@ class ColumnManager:
|
|
439
439
|
conn.commit()
|
440
440
|
|
441
441
|
def alter_column_nullable(
|
442
|
-
self,
|
442
|
+
self,
|
443
|
+
table_name: str,
|
444
|
+
column_name: str,
|
445
|
+
nullable: bool,
|
446
|
+
fill_value: Optional[Any] = None,
|
443
447
|
) -> None:
|
444
448
|
"""Change the nullable constraint on a column.
|
445
|
-
|
449
|
+
|
446
450
|
Args:
|
447
451
|
table_name: Name of the table
|
448
452
|
column_name: Name of the column to modify
|
449
453
|
nullable: Whether the column should allow NULL values
|
450
454
|
fill_value: Value to use for existing NULL values when making column NOT NULL
|
451
|
-
|
455
|
+
|
452
456
|
Raises:
|
453
457
|
ValueError: If table/column doesn't exist or column is protected
|
454
458
|
ValueError: If making NOT NULL and column has NULL values without fill_value
|
@@ -469,13 +473,13 @@ class ColumnManager:
|
|
469
473
|
existing_columns = self.list_columns(table_name)
|
470
474
|
column_found = False
|
471
475
|
old_column = None
|
472
|
-
|
476
|
+
|
473
477
|
for col in existing_columns:
|
474
478
|
if col.name == column_name:
|
475
479
|
column_found = True
|
476
480
|
old_column = col
|
477
481
|
break
|
478
|
-
|
482
|
+
|
479
483
|
if not column_found:
|
480
484
|
raise ValueError(
|
481
485
|
f"Column '{column_name}' does not exist in table '{table_name}'"
|
@@ -494,7 +498,7 @@ class ColumnManager:
|
|
494
498
|
f"SELECT COUNT(*) FROM {table_name} WHERE {column_name} IS NULL"
|
495
499
|
)
|
496
500
|
null_count = cursor.fetchone()[0]
|
497
|
-
|
501
|
+
|
498
502
|
if null_count > 0 and fill_value is None:
|
499
503
|
raise ValueError(
|
500
504
|
f"Column '{column_name}' has {null_count} NULL values. "
|
@@ -503,7 +507,7 @@ class ColumnManager:
|
|
503
507
|
|
504
508
|
# Build SQL statements for table recreation
|
505
509
|
temp_table = f"{table_name}_temp"
|
506
|
-
|
510
|
+
|
507
511
|
# Create new table with modified column
|
508
512
|
col_defs = []
|
509
513
|
for col in existing_columns:
|
@@ -527,7 +531,7 @@ class ColumnManager:
|
|
527
531
|
# Column names for copying
|
528
532
|
col_names = [col.name for col in existing_columns]
|
529
533
|
col_list = ", ".join(col_names)
|
530
|
-
|
534
|
+
|
531
535
|
# Build copy SQL with COALESCE if needed
|
532
536
|
if not nullable and fill_value is not None:
|
533
537
|
# Build select list with COALESCE for the target column
|
@@ -545,7 +549,7 @@ class ColumnManager:
|
|
545
549
|
copy_sql = f"INSERT INTO {temp_table} ({col_list}) SELECT {select_list} FROM {table_name}"
|
546
550
|
else:
|
547
551
|
copy_sql = f"INSERT INTO {temp_table} ({col_list}) SELECT {col_list} FROM {table_name}"
|
548
|
-
|
552
|
+
|
549
553
|
drop_sql = f"DROP TABLE {table_name}"
|
550
554
|
rename_sql = f"ALTER TABLE {temp_table} RENAME TO {table_name}"
|
551
555
|
|
cinchdb/managers/query.py
CHANGED
@@ -7,7 +7,7 @@ from pydantic import BaseModel, ValidationError
|
|
7
7
|
|
8
8
|
from cinchdb.core.connection import DatabaseConnection
|
9
9
|
from cinchdb.core.path_utils import get_tenant_db_path
|
10
|
-
from cinchdb.utils import validate_query_safe
|
10
|
+
from cinchdb.utils import validate_query_safe
|
11
11
|
|
12
12
|
T = TypeVar("T", bound=BaseModel)
|
13
13
|
|
@@ -33,7 +33,10 @@ class QueryManager:
|
|
33
33
|
self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
|
34
34
|
|
35
35
|
def execute(
|
36
|
-
self,
|
36
|
+
self,
|
37
|
+
sql: str,
|
38
|
+
params: Optional[Union[tuple, dict]] = None,
|
39
|
+
skip_validation: bool = False,
|
37
40
|
) -> List[Dict[str, Any]]:
|
38
41
|
"""Execute a SQL query and return results as dictionaries.
|
39
42
|
|
@@ -52,7 +55,7 @@ class QueryManager:
|
|
52
55
|
# Validate query unless explicitly skipped
|
53
56
|
if not skip_validation:
|
54
57
|
validate_query_safe(sql)
|
55
|
-
|
58
|
+
|
56
59
|
# Note: The original code had SELECT-only validation, but we're now more permissive
|
57
60
|
if not sql.strip().upper().startswith("SELECT"):
|
58
61
|
raise ValueError(
|
@@ -161,7 +164,10 @@ class QueryManager:
|
|
161
164
|
return results[0] if results else None
|
162
165
|
|
163
166
|
def execute_non_query(
|
164
|
-
self,
|
167
|
+
self,
|
168
|
+
sql: str,
|
169
|
+
params: Optional[Union[tuple, dict]] = None,
|
170
|
+
skip_validation: bool = False,
|
165
171
|
) -> int:
|
166
172
|
"""Execute a non-SELECT SQL query (INSERT, UPDATE, DELETE, etc.).
|
167
173
|
|
@@ -172,7 +178,7 @@ class QueryManager:
|
|
172
178
|
|
173
179
|
Returns:
|
174
180
|
Number of rows affected
|
175
|
-
|
181
|
+
|
176
182
|
Raises:
|
177
183
|
SQLValidationError: If query contains restricted operations
|
178
184
|
Exception: If query execution fails
|
@@ -180,7 +186,7 @@ class QueryManager:
|
|
180
186
|
# Validate query unless explicitly skipped
|
181
187
|
if not skip_validation:
|
182
188
|
validate_query_safe(sql)
|
183
|
-
|
189
|
+
|
184
190
|
with DatabaseConnection(self.db_path) as conn:
|
185
191
|
cursor = conn.execute(sql, params)
|
186
192
|
affected_rows = cursor.rowcount
|
cinchdb/managers/table.py
CHANGED
@@ -95,13 +95,13 @@ class TableManager:
|
|
95
95
|
for column in columns:
|
96
96
|
if column.foreign_key:
|
97
97
|
fk = column.foreign_key
|
98
|
-
|
98
|
+
|
99
99
|
# Validate referenced table exists
|
100
100
|
if not self._table_exists(fk.table):
|
101
101
|
raise ValueError(
|
102
102
|
f"Foreign key reference to non-existent table: '{fk.table}'"
|
103
103
|
)
|
104
|
-
|
104
|
+
|
105
105
|
# Validate referenced column exists
|
106
106
|
ref_table = self.get_table(fk.table)
|
107
107
|
ref_col_names = [col.name for col in ref_table.columns]
|
@@ -109,9 +109,11 @@ class TableManager:
|
|
109
109
|
raise ValueError(
|
110
110
|
f"Foreign key reference to non-existent column: '{fk.table}.{fk.column}'"
|
111
111
|
)
|
112
|
-
|
112
|
+
|
113
113
|
# Build foreign key constraint
|
114
|
-
fk_constraint =
|
114
|
+
fk_constraint = (
|
115
|
+
f"FOREIGN KEY ({column.name}) REFERENCES {fk.table}({fk.column})"
|
116
|
+
)
|
115
117
|
if fk.on_delete != "RESTRICT":
|
116
118
|
fk_constraint += f" ON DELETE {fk.on_delete}"
|
117
119
|
if fk.on_update != "RESTRICT":
|
@@ -143,7 +145,7 @@ class TableManager:
|
|
143
145
|
col_def += " UNIQUE"
|
144
146
|
|
145
147
|
sql_parts.append(col_def)
|
146
|
-
|
148
|
+
|
147
149
|
# Add foreign key constraints
|
148
150
|
sql_parts.extend(foreign_key_constraints)
|
149
151
|
|
@@ -201,16 +203,17 @@ class TableManager:
|
|
201
203
|
to_col = fk_row["to"]
|
202
204
|
on_update = fk_row["on_update"]
|
203
205
|
on_delete = fk_row["on_delete"]
|
204
|
-
|
206
|
+
|
205
207
|
# Create ForeignKeyRef
|
206
208
|
from cinchdb.models import ForeignKeyRef
|
209
|
+
|
207
210
|
foreign_keys[from_col] = ForeignKeyRef(
|
208
211
|
table=to_table,
|
209
212
|
column=to_col,
|
210
213
|
on_update=on_update,
|
211
|
-
on_delete=on_delete
|
214
|
+
on_delete=on_delete,
|
212
215
|
)
|
213
|
-
|
216
|
+
|
214
217
|
# Get column information
|
215
218
|
cursor = conn.execute(f"PRAGMA table_info({table_name})")
|
216
219
|
|
@@ -241,7 +244,7 @@ class TableManager:
|
|
241
244
|
nullable=(row["notnull"] == 0),
|
242
245
|
default=row["dflt_value"],
|
243
246
|
primary_key=(row["pk"] == 1),
|
244
|
-
foreign_key=foreign_key
|
247
|
+
foreign_key=foreign_key,
|
245
248
|
)
|
246
249
|
columns.append(column)
|
247
250
|
|
cinchdb/managers/tenant.py
CHANGED
@@ -70,7 +70,7 @@ class TenantManager:
|
|
70
70
|
"""
|
71
71
|
# Validate tenant name
|
72
72
|
validate_name(tenant_name, "tenant")
|
73
|
-
|
73
|
+
|
74
74
|
# Check maintenance mode
|
75
75
|
check_maintenance_mode(self.project_root, self.database, self.branch)
|
76
76
|
|
@@ -168,7 +168,7 @@ class TenantManager:
|
|
168
168
|
"""
|
169
169
|
# Validate target tenant name
|
170
170
|
validate_name(target_tenant, "tenant")
|
171
|
-
|
171
|
+
|
172
172
|
# Check maintenance mode
|
173
173
|
check_maintenance_mode(self.project_root, self.database, self.branch)
|
174
174
|
|
@@ -213,7 +213,7 @@ class TenantManager:
|
|
213
213
|
"""
|
214
214
|
# Validate new tenant name
|
215
215
|
validate_name(new_name, "tenant")
|
216
|
-
|
216
|
+
|
217
217
|
# Can't rename main tenant
|
218
218
|
if old_name == "main":
|
219
219
|
raise ValueError("Cannot rename the main tenant")
|
cinchdb/models/branch.py
CHANGED
@@ -22,7 +22,7 @@ class Branch(CinchDBBaseModel):
|
|
22
22
|
)
|
23
23
|
is_main: bool = Field(default=False, description="Whether this is the main branch")
|
24
24
|
|
25
|
-
@field_validator(
|
25
|
+
@field_validator("name")
|
26
26
|
@classmethod
|
27
27
|
def validate_name_field(cls, v: str) -> str:
|
28
28
|
"""Validate branch name meets naming requirements."""
|
cinchdb/models/database.py
CHANGED
@@ -16,7 +16,7 @@ class Database(CinchDBBaseModel):
|
|
16
16
|
active_branch: str = Field(default="main", description="Currently active branch")
|
17
17
|
description: Optional[str] = Field(default=None, description="Database description")
|
18
18
|
|
19
|
-
@field_validator(
|
19
|
+
@field_validator("name")
|
20
20
|
@classmethod
|
21
21
|
def validate_name_field(cls, v: str) -> str:
|
22
22
|
"""Validate database name meets naming requirements."""
|