cinchdb 0.1.3__py3-none-any.whl → 0.1.5__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 +25 -7
- cinchdb/core/initializer.py +214 -0
- cinchdb/managers/branch.py +1 -3
- cinchdb/managers/column.py +13 -9
- cinchdb/managers/data.py +38 -17
- 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 +61 -30
- cinchdb/utils/sql_validator.py +91 -41
- {cinchdb-0.1.3.dist-info → cinchdb-0.1.5.dist-info}/METADATA +2 -2
- cinchdb-0.1.5.dist-info/RECORD +54 -0
- cinchdb-0.1.3.dist-info/RECORD +0 -53
- {cinchdb-0.1.3.dist-info → cinchdb-0.1.5.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.3.dist-info → cinchdb-0.1.5.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.3.dist-info → cinchdb-0.1.5.dist-info}/licenses/LICENSE +0 -0
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."""
|
cinchdb/models/table.py
CHANGED
@@ -14,18 +14,16 @@ ForeignKeyAction = Literal["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]
|
|
14
14
|
|
15
15
|
class ForeignKeyRef(BaseModel):
|
16
16
|
"""Foreign key reference specification."""
|
17
|
-
|
17
|
+
|
18
18
|
model_config = ConfigDict(extra="forbid")
|
19
|
-
|
19
|
+
|
20
20
|
table: str = Field(description="Referenced table name")
|
21
21
|
column: str = Field(default="id", description="Referenced column name")
|
22
22
|
on_delete: ForeignKeyAction = Field(
|
23
|
-
default="RESTRICT",
|
24
|
-
description="Action on delete of referenced row"
|
23
|
+
default="RESTRICT", description="Action on delete of referenced row"
|
25
24
|
)
|
26
25
|
on_update: ForeignKeyAction = Field(
|
27
|
-
default="RESTRICT",
|
28
|
-
description="Action on update of referenced row"
|
26
|
+
default="RESTRICT", description="Action on update of referenced row"
|
29
27
|
)
|
30
28
|
|
31
29
|
|
@@ -47,8 +45,7 @@ class Column(BaseModel):
|
|
47
45
|
)
|
48
46
|
unique: bool = Field(default=False, description="Whether values must be unique")
|
49
47
|
foreign_key: Optional[ForeignKeyRef] = Field(
|
50
|
-
default=None,
|
51
|
-
description="Foreign key constraint specification"
|
48
|
+
default=None, description="Foreign key constraint specification"
|
52
49
|
)
|
53
50
|
|
54
51
|
|
cinchdb/models/tenant.py
CHANGED
@@ -15,7 +15,7 @@ class Tenant(CinchDBBaseModel):
|
|
15
15
|
description: Optional[str] = Field(default=None, description="Tenant description")
|
16
16
|
is_main: bool = Field(default=False, description="Whether this is the main tenant")
|
17
17
|
|
18
|
-
@field_validator(
|
18
|
+
@field_validator("name")
|
19
19
|
@classmethod
|
20
20
|
def validate_name_field(cls, v: str) -> str:
|
21
21
|
"""Validate tenant name meets naming requirements."""
|
cinchdb/utils/__init__.py
CHANGED
@@ -4,13 +4,13 @@ from cinchdb.utils.sql_validator import (
|
|
4
4
|
validate_sql_query,
|
5
5
|
validate_query_safe,
|
6
6
|
SQLValidationError,
|
7
|
-
SQLOperation
|
7
|
+
SQLOperation,
|
8
8
|
)
|
9
9
|
from cinchdb.utils.name_validator import (
|
10
10
|
validate_name,
|
11
11
|
clean_name,
|
12
12
|
is_valid_name,
|
13
|
-
InvalidNameError
|
13
|
+
InvalidNameError,
|
14
14
|
)
|
15
15
|
|
16
16
|
__all__ = [
|
@@ -19,7 +19,7 @@ __all__ = [
|
|
19
19
|
"SQLValidationError",
|
20
20
|
"SQLOperation",
|
21
21
|
"validate_name",
|
22
|
-
"clean_name",
|
22
|
+
"clean_name",
|
23
23
|
"is_valid_name",
|
24
|
-
"InvalidNameError"
|
25
|
-
]
|
24
|
+
"InvalidNameError",
|
25
|
+
]
|
cinchdb/utils/name_validator.py
CHANGED
@@ -5,55 +5,76 @@ and follow consistent naming conventions.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import re
|
8
|
-
from typing import Optional
|
9
8
|
|
10
9
|
|
11
10
|
# Regex pattern for valid names: lowercase letters, numbers, dash, underscore, period
|
12
|
-
VALID_NAME_PATTERN = re.compile(r
|
11
|
+
VALID_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-_\.]*[a-z0-9]$|^[a-z0-9]$")
|
13
12
|
|
14
13
|
# Reserved names that cannot be used
|
15
|
-
RESERVED_NAMES = {
|
16
|
-
|
17
|
-
|
14
|
+
RESERVED_NAMES = {
|
15
|
+
"con",
|
16
|
+
"prn",
|
17
|
+
"aux",
|
18
|
+
"nul",
|
19
|
+
"com1",
|
20
|
+
"com2",
|
21
|
+
"com3",
|
22
|
+
"com4",
|
23
|
+
"com5",
|
24
|
+
"com6",
|
25
|
+
"com7",
|
26
|
+
"com8",
|
27
|
+
"com9",
|
28
|
+
"lpt1",
|
29
|
+
"lpt2",
|
30
|
+
"lpt3",
|
31
|
+
"lpt4",
|
32
|
+
"lpt5",
|
33
|
+
"lpt6",
|
34
|
+
"lpt7",
|
35
|
+
"lpt8",
|
36
|
+
"lpt9",
|
37
|
+
}
|
18
38
|
|
19
39
|
|
20
40
|
class InvalidNameError(ValueError):
|
21
41
|
"""Raised when a name doesn't meet validation requirements."""
|
42
|
+
|
22
43
|
pass
|
23
44
|
|
24
45
|
|
25
46
|
def validate_name(name: str, entity_type: str = "entity") -> None:
|
26
47
|
"""Validate that a name meets CinchDB naming requirements.
|
27
|
-
|
48
|
+
|
28
49
|
Valid names must:
|
29
50
|
- Contain only lowercase letters (a-z), numbers (0-9), dash (-), and underscore (_)
|
30
51
|
- Start and end with alphanumeric characters
|
31
52
|
- Be at least 1 character long
|
32
53
|
- Not exceed 255 characters (filesystem limit)
|
33
54
|
- Not be a reserved name
|
34
|
-
|
55
|
+
|
35
56
|
Args:
|
36
57
|
name: The name to validate
|
37
58
|
entity_type: Type of entity (branch, database, tenant) for error messages
|
38
|
-
|
59
|
+
|
39
60
|
Raises:
|
40
61
|
InvalidNameError: If the name is invalid
|
41
62
|
"""
|
42
63
|
if not name:
|
43
64
|
raise InvalidNameError(f"{entity_type.capitalize()} name cannot be empty")
|
44
|
-
|
65
|
+
|
45
66
|
if len(name) > 255:
|
46
67
|
raise InvalidNameError(
|
47
68
|
f"{entity_type.capitalize()} name cannot exceed 255 characters"
|
48
69
|
)
|
49
|
-
|
70
|
+
|
50
71
|
# Check for lowercase requirement
|
51
72
|
if name != name.lower():
|
52
73
|
raise InvalidNameError(
|
53
74
|
f"{entity_type.capitalize()} name must be lowercase. "
|
54
75
|
f"Use '{name.lower()}' instead of '{name}'"
|
55
76
|
)
|
56
|
-
|
77
|
+
|
57
78
|
# Check pattern
|
58
79
|
if not VALID_NAME_PATTERN.match(name):
|
59
80
|
raise InvalidNameError(
|
@@ -62,14 +83,24 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
|
|
62
83
|
f"dash (-), underscore (_), and period (.). "
|
63
84
|
f"Names must start and end with alphanumeric characters."
|
64
85
|
)
|
65
|
-
|
86
|
+
|
66
87
|
# Check for consecutive special characters
|
67
|
-
if
|
88
|
+
if (
|
89
|
+
"--" in name
|
90
|
+
or "__" in name
|
91
|
+
or "-_" in name
|
92
|
+
or "_-" in name
|
93
|
+
or ".." in name
|
94
|
+
or ".-" in name
|
95
|
+
or "-." in name
|
96
|
+
or "._" in name
|
97
|
+
or "_." in name
|
98
|
+
):
|
68
99
|
raise InvalidNameError(
|
69
100
|
f"Invalid {entity_type} name '{name}'. "
|
70
101
|
f"Names cannot contain consecutive special characters."
|
71
102
|
)
|
72
|
-
|
103
|
+
|
73
104
|
# Check reserved names
|
74
105
|
if name.lower() in RESERVED_NAMES:
|
75
106
|
raise InvalidNameError(
|
@@ -79,46 +110,46 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
|
|
79
110
|
|
80
111
|
def clean_name(name: str) -> str:
|
81
112
|
"""Clean a name to make it valid if possible.
|
82
|
-
|
113
|
+
|
83
114
|
This performs basic cleaning:
|
84
115
|
- Convert to lowercase
|
85
116
|
- Replace spaces with dashes
|
86
117
|
- Remove invalid characters
|
87
|
-
|
118
|
+
|
88
119
|
Args:
|
89
120
|
name: The name to clean
|
90
|
-
|
121
|
+
|
91
122
|
Returns:
|
92
123
|
Cleaned name
|
93
|
-
|
124
|
+
|
94
125
|
Note:
|
95
126
|
This is a best-effort cleaning. The result should still be validated
|
96
127
|
with validate_name() before use.
|
97
128
|
"""
|
98
129
|
# Convert to lowercase
|
99
130
|
cleaned = name.lower()
|
100
|
-
|
131
|
+
|
101
132
|
# Replace spaces with dashes
|
102
|
-
cleaned = cleaned.replace(
|
103
|
-
|
133
|
+
cleaned = cleaned.replace(" ", "-")
|
134
|
+
|
104
135
|
# Remove invalid characters
|
105
|
-
cleaned = re.sub(r
|
106
|
-
|
136
|
+
cleaned = re.sub(r"[^a-z0-9\-_\.]", "", cleaned)
|
137
|
+
|
107
138
|
# Remove consecutive special characters
|
108
|
-
cleaned = re.sub(r
|
109
|
-
|
139
|
+
cleaned = re.sub(r"[-_\.]{2,}", "-", cleaned)
|
140
|
+
|
110
141
|
# Remove leading/trailing special characters
|
111
|
-
cleaned = cleaned.strip(
|
112
|
-
|
142
|
+
cleaned = cleaned.strip("-_.")
|
143
|
+
|
113
144
|
return cleaned
|
114
145
|
|
115
146
|
|
116
147
|
def is_valid_name(name: str) -> bool:
|
117
148
|
"""Check if a name is valid without raising an exception.
|
118
|
-
|
149
|
+
|
119
150
|
Args:
|
120
151
|
name: The name to check
|
121
|
-
|
152
|
+
|
122
153
|
Returns:
|
123
154
|
True if valid, False otherwise
|
124
155
|
"""
|
@@ -126,4 +157,4 @@ def is_valid_name(name: str) -> bool:
|
|
126
157
|
validate_name(name)
|
127
158
|
return True
|
128
159
|
except InvalidNameError:
|
129
|
-
return False
|
160
|
+
return False
|
cinchdb/utils/sql_validator.py
CHANGED
@@ -11,6 +11,7 @@ from enum import Enum
|
|
11
11
|
|
12
12
|
class SQLOperation(Enum):
|
13
13
|
"""Allowed SQL operations."""
|
14
|
+
|
14
15
|
SELECT = "SELECT"
|
15
16
|
INSERT = "INSERT"
|
16
17
|
UPDATE = "UPDATE"
|
@@ -19,37 +20,63 @@ class SQLOperation(Enum):
|
|
19
20
|
|
20
21
|
# List of restricted DDL operations and keywords
|
21
22
|
RESTRICTED_OPERATIONS = {
|
22
|
-
|
23
|
-
|
24
|
-
|
23
|
+
"CREATE",
|
24
|
+
"ALTER",
|
25
|
+
"DROP",
|
26
|
+
"TRUNCATE",
|
27
|
+
"RENAME",
|
28
|
+
"GRANT",
|
29
|
+
"REVOKE",
|
30
|
+
"ANALYZE",
|
31
|
+
"VACUUM",
|
32
|
+
"ATTACH",
|
33
|
+
"DETACH",
|
34
|
+
"PRAGMA",
|
35
|
+
"REINDEX",
|
36
|
+
"SAVEPOINT",
|
37
|
+
"RELEASE",
|
25
38
|
}
|
26
39
|
|
27
40
|
# Additional restricted keywords that could modify schema
|
28
41
|
RESTRICTED_KEYWORDS = {
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
42
|
+
"ADD COLUMN",
|
43
|
+
"DROP COLUMN",
|
44
|
+
"MODIFY COLUMN",
|
45
|
+
"ADD CONSTRAINT",
|
46
|
+
"DROP CONSTRAINT",
|
47
|
+
"ADD INDEX",
|
48
|
+
"DROP INDEX",
|
49
|
+
"CREATE INDEX",
|
50
|
+
"CREATE UNIQUE",
|
51
|
+
"CREATE VIEW",
|
52
|
+
"DROP VIEW",
|
53
|
+
"CREATE TRIGGER",
|
54
|
+
"DROP TRIGGER",
|
55
|
+
"CREATE PROCEDURE",
|
56
|
+
"DROP PROCEDURE",
|
57
|
+
"CREATE FUNCTION",
|
58
|
+
"DROP FUNCTION",
|
35
59
|
}
|
36
60
|
|
37
61
|
|
38
62
|
class SQLValidationError(Exception):
|
39
63
|
"""Raised when SQL query validation fails."""
|
64
|
+
|
40
65
|
pass
|
41
66
|
|
42
67
|
|
43
|
-
def validate_sql_query(
|
68
|
+
def validate_sql_query(
|
69
|
+
query: str, allow_multiple_statements: bool = False
|
70
|
+
) -> Tuple[bool, Optional[str], Optional[SQLOperation]]:
|
44
71
|
"""Validate a SQL query to ensure it only contains allowed operations.
|
45
|
-
|
72
|
+
|
46
73
|
Allowed operations: SELECT, INSERT, UPDATE, DELETE
|
47
74
|
Blocked operations: All DDL operations (CREATE, ALTER, DROP, etc.)
|
48
|
-
|
75
|
+
|
49
76
|
Args:
|
50
77
|
query: The SQL query to validate
|
51
78
|
allow_multiple_statements: Whether to allow multiple SQL statements (default: False)
|
52
|
-
|
79
|
+
|
53
80
|
Returns:
|
54
81
|
Tuple of (is_valid, error_message, operation)
|
55
82
|
- is_valid: True if query is valid
|
@@ -58,80 +85,103 @@ def validate_sql_query(query: str, allow_multiple_statements: bool = False) -> T
|
|
58
85
|
"""
|
59
86
|
if not query or not query.strip():
|
60
87
|
return False, "Query cannot be empty", None
|
61
|
-
|
88
|
+
|
62
89
|
# Normalize the query - remove comments and extra whitespace
|
63
90
|
normalized_query = query
|
64
91
|
# Remove single-line comments
|
65
|
-
normalized_query = re.sub(r
|
92
|
+
normalized_query = re.sub(r"--.*$", "", normalized_query, flags=re.MULTILINE)
|
66
93
|
# Remove multi-line comments
|
67
|
-
normalized_query = re.sub(r
|
94
|
+
normalized_query = re.sub(r"/\*[\s\S]*?\*/", "", normalized_query)
|
68
95
|
# Replace multiple spaces with single space
|
69
|
-
normalized_query = re.sub(r
|
96
|
+
normalized_query = re.sub(r"\s+", " ", normalized_query)
|
70
97
|
normalized_query = normalized_query.strip().upper()
|
71
|
-
|
98
|
+
|
72
99
|
if not normalized_query:
|
73
100
|
return False, "Query cannot be empty after removing comments", None
|
74
|
-
|
101
|
+
|
75
102
|
# Check for multiple statements (security risk)
|
76
103
|
if not allow_multiple_statements:
|
77
104
|
# Count semicolons that are not at the end
|
78
|
-
semicolon_pos = normalized_query.find(
|
105
|
+
semicolon_pos = normalized_query.find(";")
|
79
106
|
if semicolon_pos != -1 and semicolon_pos < len(normalized_query) - 1:
|
80
107
|
# Check if there's non-whitespace after the semicolon
|
81
|
-
remaining = normalized_query[semicolon_pos + 1:].strip()
|
108
|
+
remaining = normalized_query[semicolon_pos + 1 :].strip()
|
82
109
|
if remaining:
|
83
|
-
return
|
84
|
-
|
110
|
+
return (
|
111
|
+
False,
|
112
|
+
"Multiple statements are not allowed. Please execute one query at a time.",
|
113
|
+
None,
|
114
|
+
)
|
115
|
+
|
85
116
|
# Extract the first word (operation)
|
86
|
-
first_word = normalized_query.split()[0].rstrip(
|
87
|
-
|
117
|
+
first_word = normalized_query.split()[0].rstrip(";")
|
118
|
+
|
88
119
|
# Check if it's an allowed operation
|
89
120
|
try:
|
90
121
|
operation = SQLOperation(first_word)
|
91
|
-
|
122
|
+
|
92
123
|
# Additional validation for UPDATE and DELETE
|
93
124
|
if operation in (SQLOperation.UPDATE, SQLOperation.DELETE):
|
94
125
|
# Warning if no WHERE clause (we don't block it, just log)
|
95
|
-
if
|
126
|
+
if "WHERE" not in normalized_query:
|
96
127
|
import logging
|
97
|
-
|
98
|
-
|
128
|
+
|
129
|
+
logging.warning(
|
130
|
+
f"{operation.value} statement without WHERE clause detected"
|
131
|
+
)
|
132
|
+
|
99
133
|
return True, None, operation
|
100
134
|
except ValueError:
|
101
135
|
# Not a recognized allowed operation
|
102
136
|
pass
|
103
|
-
|
137
|
+
|
104
138
|
# Check for restricted operations
|
105
139
|
for restricted in RESTRICTED_OPERATIONS:
|
106
140
|
if normalized_query.startswith(restricted):
|
107
|
-
return
|
108
|
-
|
141
|
+
return (
|
142
|
+
False,
|
143
|
+
f"{restricted} operations are not allowed. Only SELECT, INSERT, UPDATE, and DELETE queries are permitted.",
|
144
|
+
None,
|
145
|
+
)
|
146
|
+
|
109
147
|
# Check for restricted keywords anywhere in the query
|
110
148
|
for keyword in RESTRICTED_KEYWORDS:
|
111
149
|
if keyword in normalized_query:
|
112
|
-
return
|
113
|
-
|
150
|
+
return (
|
151
|
+
False,
|
152
|
+
f"Query contains restricted operation: {keyword}. Only SELECT, INSERT, UPDATE, and DELETE queries are permitted.",
|
153
|
+
None,
|
154
|
+
)
|
155
|
+
|
114
156
|
# Check for WITH statements that might contain DDL
|
115
|
-
if normalized_query.startswith(
|
157
|
+
if normalized_query.startswith("WITH"):
|
116
158
|
# Check if the CTE contains any DDL operations
|
117
159
|
for restricted in RESTRICTED_OPERATIONS:
|
118
160
|
if restricted in normalized_query:
|
119
|
-
return
|
120
|
-
|
161
|
+
return (
|
162
|
+
False,
|
163
|
+
f"CTE (WITH clause) containing {restricted} operations is not allowed.",
|
164
|
+
None,
|
165
|
+
)
|
166
|
+
|
121
167
|
# If we get here, it's an unrecognized operation
|
122
|
-
return
|
168
|
+
return (
|
169
|
+
False,
|
170
|
+
"Unrecognized or restricted SQL operation. Only SELECT, INSERT, UPDATE, and DELETE queries are permitted.",
|
171
|
+
None,
|
172
|
+
)
|
123
173
|
|
124
174
|
|
125
175
|
def validate_query_safe(query: str, allow_multiple_statements: bool = False) -> None:
|
126
176
|
"""Validate a SQL query and raise an exception if invalid.
|
127
|
-
|
177
|
+
|
128
178
|
Args:
|
129
179
|
query: The SQL query to validate
|
130
180
|
allow_multiple_statements: Whether to allow multiple SQL statements
|
131
|
-
|
181
|
+
|
132
182
|
Raises:
|
133
183
|
SQLValidationError: If the query is invalid
|
134
184
|
"""
|
135
185
|
is_valid, error_message, _ = validate_sql_query(query, allow_multiple_statements)
|
136
186
|
if not is_valid:
|
137
|
-
raise SQLValidationError(error_message)
|
187
|
+
raise SQLValidationError(error_message)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cinchdb
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: A Git-like SQLite database management system with branching and multi-tenancy
|
5
5
|
Project-URL: Homepage, https://github.com/russellromney/cinchdb
|
6
6
|
Project-URL: Documentation, https://russellromney.github.io/cinchdb
|
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
|
|
31
31
|
|
32
32
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
33
33
|
|
34
|
-
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database.
|
34
|
+
CinchDB is for projects that need fast queries, data isolated data per-tenant [or even per-user](https://turso.tech/blog/give-each-of-your-users-their-own-sqlite-database-b74445f4), and a branchable database that makes it easy to merge changes between branches.
|
35
35
|
|
36
36
|
On a meta level, I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
37
37
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
cinchdb/__init__.py,sha256=NZdSzfhRguSBTjJ2dcESOQYy53OZEuBndlB7U08GMY0,179
|
2
|
+
cinchdb/__main__.py,sha256=OpkDqn9zkTZhhYgvv_grswWLAHKbmxs4M-8C6Z5HfWY,85
|
3
|
+
cinchdb/config.py,sha256=gocjMnYKLWhgvnteo6zprgwtK6Oevoxq547J_v-C9Ns,5265
|
4
|
+
cinchdb/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
+
cinchdb/cli/main.py,sha256=Icr_uhe_zXPAuhM9NB7evR5b1ZP7f_N40HQcC1JFhQ0,4706
|
6
|
+
cinchdb/cli/utils.py,sha256=NREFxN9k53FnPbDoPt4SXmdZzlzw9zUMv5ICQwTT8gk,5679
|
7
|
+
cinchdb/cli/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
|
8
|
+
cinchdb/cli/commands/branch.py,sha256=Nz8YQYJ7lizSXEAv0usTx85TDOC-N5Ul9KIxN8JQtKc,17973
|
9
|
+
cinchdb/cli/commands/codegen.py,sha256=WsRWmXNTDuaLPyECW5psXM9zOQnKHpUiv8BJnBAjMII,6189
|
10
|
+
cinchdb/cli/commands/column.py,sha256=ISHRmcoLf1fAbPqC2MaAYH7Fc6xZWtzCMSRh7_9o-lY,11757
|
11
|
+
cinchdb/cli/commands/database.py,sha256=-UCOnn3VatdNog-fX5pguJ2GKdSSXQN99-LSVwkvinY,6857
|
12
|
+
cinchdb/cli/commands/query.py,sha256=XW_YL6M5IYHHHMpVB5p-M01kawFxwDOK5B5hGIy_BA8,5044
|
13
|
+
cinchdb/cli/commands/remote.py,sha256=i07hfiAxgrROB9lVJVaKK_nWxT1SGiSbtFb4jvEwxEo,4445
|
14
|
+
cinchdb/cli/commands/table.py,sha256=NxfOTCd9beaujffiAPiW0Vko0--HS1JVeCwBMp_khx4,10518
|
15
|
+
cinchdb/cli/commands/tenant.py,sha256=cOWs9Gf13ZE4KTKJZ_AvDwFDdXBUn5i-Av4MoUsc8Go,6199
|
16
|
+
cinchdb/cli/commands/view.py,sha256=ZmS1IW7idzzHAXmgVyY3C4IQRo7toHb6fHNFY_tQJjI,6385
|
17
|
+
cinchdb/cli/handlers/__init__.py,sha256=f2f-Cc96rSBLbVsiIbf-b4pZCKZoHfmhNEvnZ0OurRs,131
|
18
|
+
cinchdb/cli/handlers/codegen_handler.py,sha256=i5we_AbiUW3zfO6pIKWxvtO8OvOqz3H__4xPmTLEuQM,6524
|
19
|
+
cinchdb/core/__init__.py,sha256=iNlT0iO9cM0HLoYwzBavUBoXRh1Tcnz1l_vfbwVxK_Q,246
|
20
|
+
cinchdb/core/connection.py,sha256=SlKyEfIpeaDws8M6SfEbvCEVnt26zBY1RYwHtTXj0kY,5110
|
21
|
+
cinchdb/core/database.py,sha256=g-QH8NOWvegajAwplNHcooPPpvZEr7L7NfrTIqlqJN8,18591
|
22
|
+
cinchdb/core/initializer.py,sha256=CjnJSMuR1NrHobyFfwL44tUeH8VE62q02bijEtVH3p4,6922
|
23
|
+
cinchdb/core/maintenance.py,sha256=PAgrSL7Cj9p3rKHV0h_L7gupN6nLD0-5eQpJZNiqyEs,2097
|
24
|
+
cinchdb/core/path_utils.py,sha256=J2UEu1X_NFOqDamcsrPrC7ZitGTg9Y-HFjmx4sHf5j8,3806
|
25
|
+
cinchdb/managers/__init__.py,sha256=ic61ZUdsg-muq0ETYO6fuZRQWF4j7l920PthTkt2QrE,808
|
26
|
+
cinchdb/managers/branch.py,sha256=FkF2i5vZ8ifldGm9tLcgPNymifFdBbpCrfLorIBYCiE,5330
|
27
|
+
cinchdb/managers/change_applier.py,sha256=cHPhPgbJ9jeyrb6lkfRyumS8IHat0HiWfwZh-n7ButA,14310
|
28
|
+
cinchdb/managers/change_comparator.py,sha256=08pwybpSt36cFwhZRSIkHynvFMUaLKEVwa8Ajn_R9yQ,6862
|
29
|
+
cinchdb/managers/change_tracker.py,sha256=U93BPnuGv8xSaO5qr_y5Q8ppKrVXygozdp5zUvLUqwg,5054
|
30
|
+
cinchdb/managers/codegen.py,sha256=1CfIwjgHnNDdjrq4SzQ9VE7DFgnWfk7RtpupBFUTqxk,21804
|
31
|
+
cinchdb/managers/column.py,sha256=YhYq-hnH0o2BqZkyihnsY5KIWEztzs-_iLJNZMdVUkk,20807
|
32
|
+
cinchdb/managers/data.py,sha256=zS1HkMGf436m6f8VdFAqQbQFgo4sL5yKJRcRf4A6lIc,16253
|
33
|
+
cinchdb/managers/merge_manager.py,sha256=R8S2hLkLJg4hLDpeJTzjVkduZgqPOjXtYgOSJhTXXrE,15690
|
34
|
+
cinchdb/managers/query.py,sha256=pBlbqoovnFsZ36pB7nv8NtzcTFwtT26hp8IlwjIx29Q,7301
|
35
|
+
cinchdb/managers/table.py,sha256=KWSAfZCJafKZPx-dRG7KwQUGVqVQ56ARMVBllb3VBig,13114
|
36
|
+
cinchdb/managers/tenant.py,sha256=QONC5R8tW5CJoGUxECJmQyiSP12mz3rZBmRqxCsKjmM,8726
|
37
|
+
cinchdb/managers/view.py,sha256=v9gYtRufZyxywPKLGvIjvlUXcxYh9CLRArefu9QX6zk,7809
|
38
|
+
cinchdb/models/__init__.py,sha256=382OuS0BaKPA71GjqNW5lfVhtUYqmcMlLRin7HPi6XI,602
|
39
|
+
cinchdb/models/base.py,sha256=7j4rlFTP5K9ZuF8vxwC7lMFEaL7O90NJ47Ig5i7ubcw,1320
|
40
|
+
cinchdb/models/branch.py,sha256=gRgLpRFkMC3fxf9ZigVOkS6wdkBERWqlLk0_gOYjqNk,1180
|
41
|
+
cinchdb/models/change.py,sha256=YpBWdI6yMT3uucd8duET9s75xr5JUWJqurkkyTlXPlk,1449
|
42
|
+
cinchdb/models/database.py,sha256=QrWd_SkE1G8TMWflO4sXRUbSdbqcrfGOt2e-PS7OW7A,971
|
43
|
+
cinchdb/models/project.py,sha256=6GMXUZUsEIebqQJgRXIthWzpWKuNNmJ3drgI1vFDrMo,644
|
44
|
+
cinchdb/models/table.py,sha256=k2_nnyW1E6-UM2Zw49K5njP_fxySW3HVWymcR_a0e_0,2759
|
45
|
+
cinchdb/models/tenant.py,sha256=UKYTKM4mQH3IqEjI_tOU5CszwBWH4cXa3lI0mpMFF_4,967
|
46
|
+
cinchdb/models/view.py,sha256=q6j-jYzFJuhRJO87rKt6Uv8hOizHQx8xwoPKoH6XnNY,530
|
47
|
+
cinchdb/utils/__init__.py,sha256=yQQhEjndDiB2SUJybUmp9dvEOQKiR-GySe-WiCius5E,490
|
48
|
+
cinchdb/utils/name_validator.py,sha256=dyGX5bjlTFRA9EGrWRQKp6kR__HSV04hLV5VueJs4IQ,4027
|
49
|
+
cinchdb/utils/sql_validator.py,sha256=aWOGlPX0gBkuR6R1EBP2stbP4PHZuI6FUBi2Ljx7JUI,5815
|
50
|
+
cinchdb-0.1.5.dist-info/METADATA,sha256=pnIHIDE116wBD3rp7oNJ_45l9NVxV0Qg7V0QyarGZvo,5855
|
51
|
+
cinchdb-0.1.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
52
|
+
cinchdb-0.1.5.dist-info/entry_points.txt,sha256=VBOIzvnGbkKudMCCmNORS3885QSyjZUVKJQ-Syqa62w,47
|
53
|
+
cinchdb-0.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
54
|
+
cinchdb-0.1.5.dist-info/RECORD,,
|