cinchdb 0.1.0__tar.gz → 0.1.2__tar.gz
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-0.1.0 → cinchdb-0.1.2}/.gitignore +3 -1
- {cinchdb-0.1.0 → cinchdb-0.1.2}/PKG-INFO +17 -19
- {cinchdb-0.1.0 → cinchdb-0.1.2}/README.md +16 -15
- {cinchdb-0.1.0 → cinchdb-0.1.2}/pyproject.toml +1 -5
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/branch.py +9 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/database.py +9 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/tenant.py +17 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/branch.py +5 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/tenant.py +13 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/branch.py +9 -1
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/database.py +9 -1
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/tenant.py +9 -1
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/utils/__init__.py +11 -1
- cinchdb-0.1.2/src/cinchdb/utils/name_validator.py +129 -0
- cinchdb-0.1.0/src/cinchdb/api/__init__.py +0 -5
- cinchdb-0.1.0/src/cinchdb/api/app.py +0 -76
- cinchdb-0.1.0/src/cinchdb/api/auth.py +0 -290
- cinchdb-0.1.0/src/cinchdb/api/main.py +0 -137
- cinchdb-0.1.0/src/cinchdb/api/routers/__init__.py +0 -25
- cinchdb-0.1.0/src/cinchdb/api/routers/auth.py +0 -135
- cinchdb-0.1.0/src/cinchdb/api/routers/branches.py +0 -368
- cinchdb-0.1.0/src/cinchdb/api/routers/codegen.py +0 -164
- cinchdb-0.1.0/src/cinchdb/api/routers/columns.py +0 -290
- cinchdb-0.1.0/src/cinchdb/api/routers/data.py +0 -479
- cinchdb-0.1.0/src/cinchdb/api/routers/databases.py +0 -177
- cinchdb-0.1.0/src/cinchdb/api/routers/projects.py +0 -133
- cinchdb-0.1.0/src/cinchdb/api/routers/query.py +0 -156
- cinchdb-0.1.0/src/cinchdb/api/routers/tables.py +0 -349
- cinchdb-0.1.0/src/cinchdb/api/routers/tenants.py +0 -216
- cinchdb-0.1.0/src/cinchdb/api/routers/views.py +0 -219
- {cinchdb-0.1.0 → cinchdb-0.1.2}/LICENSE +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/__main__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/codegen.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/column.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/query.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/remote.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/table.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/commands/view.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/handlers/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/main.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/cli/utils.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/config.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/core/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/core/connection.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/core/database.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/core/maintenance.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/core/path_utils.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/change_applier.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/change_comparator.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/change_tracker.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/codegen.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/column.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/data.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/merge_manager.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/query.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/table.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/managers/view.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/__init__.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/base.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/change.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/project.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/table.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/models/view.py +0 -0
- {cinchdb-0.1.0 → cinchdb-0.1.2}/src/cinchdb/utils/sql_validator.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cinchdb
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.2
|
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
|
@@ -18,14 +18,11 @@ Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
20
20
|
Requires-Python: >=3.10
|
21
|
-
Requires-Dist: fastapi>=0.115.0
|
22
21
|
Requires-Dist: pydantic>=2.0.0
|
23
|
-
Requires-Dist: python-multipart>=0.0.12
|
24
22
|
Requires-Dist: requests>=2.28.0
|
25
23
|
Requires-Dist: rich>=13.0.0
|
26
24
|
Requires-Dist: toml>=0.10.0
|
27
25
|
Requires-Dist: typer>=0.9.0
|
28
|
-
Requires-Dist: uvicorn>=0.32.0
|
29
26
|
Description-Content-Type: text/markdown
|
30
27
|
|
31
28
|
# CinchDB
|
@@ -34,11 +31,11 @@ Description-Content-Type: text/markdown
|
|
34
31
|
|
35
32
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
36
33
|
|
37
|
-
CinchDB is for projects that need
|
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.
|
38
35
|
|
39
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.
|
40
37
|
|
41
|
-
Because it's so lightweight and its only dependencies are
|
38
|
+
Because it's so lightweight and its only dependencies are pydantic, requests, and Typer, it makes for a perfect local development database that can be controlled programmatically.
|
42
39
|
|
43
40
|
|
44
41
|
```bash
|
@@ -61,9 +58,9 @@ cinch branch merge-into-main feature
|
|
61
58
|
cinch tenant create customer_a
|
62
59
|
cinch query "SELECT * FROM users" --tenant customer_a
|
63
60
|
|
64
|
-
#
|
65
|
-
|
66
|
-
cinch
|
61
|
+
# Connect to remote CinchDB instance
|
62
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
63
|
+
cinch remote use production
|
67
64
|
|
68
65
|
# Autogenerate Python SDK from database
|
69
66
|
cinch codegen generate python cinchdb_models/
|
@@ -77,9 +74,9 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
|
|
77
74
|
- **Multi-tenant isolation** - shared schema, isolated data per tenant
|
78
75
|
- **Automatic change tracking** - all schema changes tracked and mergeable
|
79
76
|
- **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
|
80
|
-
- **Remote
|
77
|
+
- **Remote connectivity** - Connect to hosted CinchDB instances
|
81
78
|
- **Type-safe SDK** - Python and TypeScript SDKs with full type safety
|
82
|
-
- **
|
79
|
+
- **Remote-capable** - CLI and SDK can connect to remote instances
|
83
80
|
- **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
|
84
81
|
|
85
82
|
## Installation
|
@@ -136,24 +133,25 @@ post_id = db.insert("posts", {"title": "Hello World", "content": "First post"})
|
|
136
133
|
db.update("posts", post_id, {"content": "Updated content"})
|
137
134
|
```
|
138
135
|
|
139
|
-
### Remote
|
136
|
+
### Remote Connection
|
140
137
|
|
141
138
|
```python
|
142
|
-
# Connect to remote
|
143
|
-
db = cinchdb.
|
139
|
+
# Connect to remote instance
|
140
|
+
db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
|
144
141
|
|
145
142
|
# Same interface as local
|
146
143
|
results = db.query("SELECT * FROM users")
|
147
144
|
user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
|
148
145
|
```
|
149
146
|
|
150
|
-
##
|
147
|
+
## Remote Access
|
151
148
|
|
152
|
-
|
149
|
+
Connect to a remote CinchDB instance:
|
153
150
|
|
154
151
|
```bash
|
155
|
-
cinch-server
|
156
|
-
|
152
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
153
|
+
cinch remote use production
|
154
|
+
# Now all commands will use the remote instance
|
157
155
|
```
|
158
156
|
|
159
157
|
Interactive docs at `/docs`, health check at `/health`.
|
@@ -162,7 +160,7 @@ Interactive docs at `/docs`, health check at `/health`.
|
|
162
160
|
|
163
161
|
- **Python SDK**: Core functionality (local + remote)
|
164
162
|
- **CLI**: Full-featured command-line interface
|
165
|
-
- **
|
163
|
+
- **Remote Access**: Connect to hosted CinchDB instances
|
166
164
|
- **TypeScript SDK**: Browser and Node.js client
|
167
165
|
|
168
166
|
## Development
|
@@ -4,11 +4,11 @@
|
|
4
4
|
|
5
5
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
6
6
|
|
7
|
-
CinchDB is for projects that need
|
7
|
+
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.
|
8
8
|
|
9
9
|
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.
|
10
10
|
|
11
|
-
Because it's so lightweight and its only dependencies are
|
11
|
+
Because it's so lightweight and its only dependencies are pydantic, requests, and Typer, it makes for a perfect local development database that can be controlled programmatically.
|
12
12
|
|
13
13
|
|
14
14
|
```bash
|
@@ -31,9 +31,9 @@ cinch branch merge-into-main feature
|
|
31
31
|
cinch tenant create customer_a
|
32
32
|
cinch query "SELECT * FROM users" --tenant customer_a
|
33
33
|
|
34
|
-
#
|
35
|
-
|
36
|
-
cinch
|
34
|
+
# Connect to remote CinchDB instance
|
35
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
36
|
+
cinch remote use production
|
37
37
|
|
38
38
|
# Autogenerate Python SDK from database
|
39
39
|
cinch codegen generate python cinchdb_models/
|
@@ -47,9 +47,9 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
|
|
47
47
|
- **Multi-tenant isolation** - shared schema, isolated data per tenant
|
48
48
|
- **Automatic change tracking** - all schema changes tracked and mergeable
|
49
49
|
- **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
|
50
|
-
- **Remote
|
50
|
+
- **Remote connectivity** - Connect to hosted CinchDB instances
|
51
51
|
- **Type-safe SDK** - Python and TypeScript SDKs with full type safety
|
52
|
-
- **
|
52
|
+
- **Remote-capable** - CLI and SDK can connect to remote instances
|
53
53
|
- **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
|
54
54
|
|
55
55
|
## Installation
|
@@ -106,24 +106,25 @@ post_id = db.insert("posts", {"title": "Hello World", "content": "First post"})
|
|
106
106
|
db.update("posts", post_id, {"content": "Updated content"})
|
107
107
|
```
|
108
108
|
|
109
|
-
### Remote
|
109
|
+
### Remote Connection
|
110
110
|
|
111
111
|
```python
|
112
|
-
# Connect to remote
|
113
|
-
db = cinchdb.
|
112
|
+
# Connect to remote instance
|
113
|
+
db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
|
114
114
|
|
115
115
|
# Same interface as local
|
116
116
|
results = db.query("SELECT * FROM users")
|
117
117
|
user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
|
118
118
|
```
|
119
119
|
|
120
|
-
##
|
120
|
+
## Remote Access
|
121
121
|
|
122
|
-
|
122
|
+
Connect to a remote CinchDB instance:
|
123
123
|
|
124
124
|
```bash
|
125
|
-
cinch-server
|
126
|
-
|
125
|
+
cinch remote add production https://your-cinchdb-server.com your-api-key
|
126
|
+
cinch remote use production
|
127
|
+
# Now all commands will use the remote instance
|
127
128
|
```
|
128
129
|
|
129
130
|
Interactive docs at `/docs`, health check at `/health`.
|
@@ -132,7 +133,7 @@ Interactive docs at `/docs`, health check at `/health`.
|
|
132
133
|
|
133
134
|
- **Python SDK**: Core functionality (local + remote)
|
134
135
|
- **CLI**: Full-featured command-line interface
|
135
|
-
- **
|
136
|
+
- **Remote Access**: Connect to hosted CinchDB instances
|
136
137
|
- **TypeScript SDK**: Browser and Node.js client
|
137
138
|
|
138
139
|
## Development
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "cinchdb"
|
3
|
-
version = "0.1.
|
3
|
+
version = "0.1.2"
|
4
4
|
description = "A Git-like SQLite database management system with branching and multi-tenancy"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.10"
|
@@ -23,9 +23,6 @@ dependencies = [
|
|
23
23
|
"toml>=0.10.0",
|
24
24
|
"typer>=0.9.0",
|
25
25
|
"rich>=13.0.0",
|
26
|
-
"fastapi>=0.115.0",
|
27
|
-
"uvicorn>=0.32.0",
|
28
|
-
"python-multipart>=0.0.12",
|
29
26
|
"requests>=2.28.0",
|
30
27
|
]
|
31
28
|
|
@@ -37,7 +34,6 @@ Issues = "https://github.com/russellromney/cinchdb/issues"
|
|
37
34
|
|
38
35
|
[project.scripts]
|
39
36
|
cinch = "cinchdb.cli.main:app"
|
40
|
-
cinch-server = "cinchdb.api.main:cli"
|
41
37
|
|
42
38
|
[build-system]
|
43
39
|
requires = ["hatchling"]
|
@@ -14,6 +14,7 @@ from cinchdb.cli.utils import (
|
|
14
14
|
set_active_branch,
|
15
15
|
validate_required_arg,
|
16
16
|
)
|
17
|
+
from cinchdb.utils.name_validator import validate_name, InvalidNameError
|
17
18
|
|
18
19
|
app = typer.Typer(help="Branch management commands", invoke_without_command=True)
|
19
20
|
console = Console()
|
@@ -80,6 +81,14 @@ def create(
|
|
80
81
|
):
|
81
82
|
"""Create a new branch."""
|
82
83
|
name = validate_required_arg(name, "name", ctx)
|
84
|
+
|
85
|
+
# Validate branch name
|
86
|
+
try:
|
87
|
+
validate_name(name, "branch")
|
88
|
+
except InvalidNameError as e:
|
89
|
+
console.print(f"[red]❌ {e}[/red]")
|
90
|
+
raise typer.Exit(1)
|
91
|
+
|
83
92
|
config, config_data = get_config_with_data()
|
84
93
|
db_name = config_data.active_database
|
85
94
|
source_branch = source or config_data.active_branch
|
@@ -12,6 +12,7 @@ from cinchdb.cli.utils import (
|
|
12
12
|
set_active_branch,
|
13
13
|
validate_required_arg,
|
14
14
|
)
|
15
|
+
from cinchdb.utils.name_validator import validate_name, InvalidNameError
|
15
16
|
|
16
17
|
app = typer.Typer(help="Database management commands", invoke_without_command=True)
|
17
18
|
console = Console()
|
@@ -64,6 +65,14 @@ def create(
|
|
64
65
|
):
|
65
66
|
"""Create a new database."""
|
66
67
|
name = validate_required_arg(name, "name", ctx)
|
68
|
+
|
69
|
+
# Validate database name
|
70
|
+
try:
|
71
|
+
validate_name(name, "database")
|
72
|
+
except InvalidNameError as e:
|
73
|
+
console.print(f"[red]❌ {e}[/red]")
|
74
|
+
raise typer.Exit(1)
|
75
|
+
|
67
76
|
config, config_data = get_config_with_data()
|
68
77
|
|
69
78
|
# Create database directory structure
|
@@ -10,6 +10,7 @@ from cinchdb.config import Config
|
|
10
10
|
from cinchdb.core.path_utils import get_project_root
|
11
11
|
from cinchdb.managers.tenant import TenantManager
|
12
12
|
from cinchdb.cli.utils import get_config_with_data, validate_required_arg
|
13
|
+
from cinchdb.utils.name_validator import validate_name, InvalidNameError
|
13
14
|
|
14
15
|
app = typer.Typer(help="Tenant management commands", invoke_without_command=True)
|
15
16
|
console = Console()
|
@@ -70,6 +71,14 @@ def create(
|
|
70
71
|
):
|
71
72
|
"""Create a new tenant."""
|
72
73
|
name = validate_required_arg(name, "name", ctx)
|
74
|
+
|
75
|
+
# Validate tenant name
|
76
|
+
try:
|
77
|
+
validate_name(name, "tenant")
|
78
|
+
except InvalidNameError as e:
|
79
|
+
console.print(f"[red]❌ {e}[/red]")
|
80
|
+
raise typer.Exit(1)
|
81
|
+
|
73
82
|
config, config_data = get_config_with_data()
|
74
83
|
db_name = config_data.active_database
|
75
84
|
branch_name = config_data.active_branch
|
@@ -155,6 +164,14 @@ def rename(
|
|
155
164
|
"""Rename a tenant."""
|
156
165
|
old_name = validate_required_arg(old_name, "old_name", ctx)
|
157
166
|
new_name = validate_required_arg(new_name, "new_name", ctx)
|
167
|
+
|
168
|
+
# Validate new tenant name
|
169
|
+
try:
|
170
|
+
validate_name(new_name, "tenant")
|
171
|
+
except InvalidNameError as e:
|
172
|
+
console.print(f"[red]❌ {e}[/red]")
|
173
|
+
raise typer.Exit(1)
|
174
|
+
|
158
175
|
config, config_data = get_config_with_data()
|
159
176
|
db_name = config_data.active_database
|
160
177
|
branch_name = config_data.active_branch
|
@@ -13,6 +13,7 @@ from cinchdb.core.path_utils import (
|
|
13
13
|
get_branch_path,
|
14
14
|
list_branches,
|
15
15
|
)
|
16
|
+
from cinchdb.utils.name_validator import validate_name
|
16
17
|
|
17
18
|
|
18
19
|
class BranchManager:
|
@@ -63,7 +64,11 @@ class BranchManager:
|
|
63
64
|
|
64
65
|
Raises:
|
65
66
|
ValueError: If source doesn't exist or new branch already exists
|
67
|
+
InvalidNameError: If new branch name is invalid
|
66
68
|
"""
|
69
|
+
# Validate new branch name
|
70
|
+
validate_name(new_branch_name, "branch")
|
71
|
+
|
67
72
|
# Validate source branch exists
|
68
73
|
if source_branch not in list_branches(self.project_root, self.database):
|
69
74
|
raise ValueError(f"Source branch '{source_branch}' does not exist")
|
@@ -12,6 +12,7 @@ from cinchdb.core.path_utils import (
|
|
12
12
|
)
|
13
13
|
from cinchdb.core.connection import DatabaseConnection
|
14
14
|
from cinchdb.core.maintenance import check_maintenance_mode
|
15
|
+
from cinchdb.utils.name_validator import validate_name
|
15
16
|
|
16
17
|
|
17
18
|
class TenantManager:
|
@@ -64,8 +65,12 @@ class TenantManager:
|
|
64
65
|
|
65
66
|
Raises:
|
66
67
|
ValueError: If tenant already exists
|
68
|
+
InvalidNameError: If tenant name is invalid
|
67
69
|
MaintenanceError: If branch is in maintenance mode
|
68
70
|
"""
|
71
|
+
# Validate tenant name
|
72
|
+
validate_name(tenant_name, "tenant")
|
73
|
+
|
69
74
|
# Check maintenance mode
|
70
75
|
check_maintenance_mode(self.project_root, self.database, self.branch)
|
71
76
|
|
@@ -158,8 +163,12 @@ class TenantManager:
|
|
158
163
|
|
159
164
|
Raises:
|
160
165
|
ValueError: If source doesn't exist or target already exists
|
166
|
+
InvalidNameError: If target tenant name is invalid
|
161
167
|
MaintenanceError: If branch is in maintenance mode
|
162
168
|
"""
|
169
|
+
# Validate target tenant name
|
170
|
+
validate_name(target_tenant, "tenant")
|
171
|
+
|
163
172
|
# Check maintenance mode
|
164
173
|
check_maintenance_mode(self.project_root, self.database, self.branch)
|
165
174
|
|
@@ -200,7 +209,11 @@ class TenantManager:
|
|
200
209
|
|
201
210
|
Raises:
|
202
211
|
ValueError: If old doesn't exist, new already exists, or trying to rename main
|
212
|
+
InvalidNameError: If new tenant name is invalid
|
203
213
|
"""
|
214
|
+
# Validate new tenant name
|
215
|
+
validate_name(new_name, "tenant")
|
216
|
+
|
204
217
|
# Can't rename main tenant
|
205
218
|
if old_name == "main":
|
206
219
|
raise ValueError("Cannot rename the main tenant")
|
@@ -1,8 +1,9 @@
|
|
1
1
|
"""Branch model for CinchDB."""
|
2
2
|
|
3
3
|
from typing import List, Optional, Dict, Any
|
4
|
-
from pydantic import Field
|
4
|
+
from pydantic import Field, field_validator
|
5
5
|
from .base import CinchDBBaseModel
|
6
|
+
from ..utils.name_validator import validate_name
|
6
7
|
|
7
8
|
|
8
9
|
class Branch(CinchDBBaseModel):
|
@@ -21,6 +22,13 @@ class Branch(CinchDBBaseModel):
|
|
21
22
|
)
|
22
23
|
is_main: bool = Field(default=False, description="Whether this is the main branch")
|
23
24
|
|
25
|
+
@field_validator('name')
|
26
|
+
@classmethod
|
27
|
+
def validate_name_field(cls, v: str) -> str:
|
28
|
+
"""Validate branch name meets naming requirements."""
|
29
|
+
validate_name(v, "branch")
|
30
|
+
return v
|
31
|
+
|
24
32
|
def can_delete(self) -> bool:
|
25
33
|
"""Check if this branch can be deleted."""
|
26
34
|
return self.name != "main" and not self.is_main
|
@@ -1,8 +1,9 @@
|
|
1
1
|
"""Database model for CinchDB."""
|
2
2
|
|
3
3
|
from typing import List, Optional
|
4
|
-
from pydantic import Field
|
4
|
+
from pydantic import Field, field_validator
|
5
5
|
from .base import CinchDBBaseModel
|
6
|
+
from ..utils.name_validator import validate_name
|
6
7
|
|
7
8
|
|
8
9
|
class Database(CinchDBBaseModel):
|
@@ -15,6 +16,13 @@ class Database(CinchDBBaseModel):
|
|
15
16
|
active_branch: str = Field(default="main", description="Currently active branch")
|
16
17
|
description: Optional[str] = Field(default=None, description="Database description")
|
17
18
|
|
19
|
+
@field_validator('name')
|
20
|
+
@classmethod
|
21
|
+
def validate_name_field(cls, v: str) -> str:
|
22
|
+
"""Validate database name meets naming requirements."""
|
23
|
+
validate_name(v, "database")
|
24
|
+
return v
|
25
|
+
|
18
26
|
def can_delete(self) -> bool:
|
19
27
|
"""Check if this database can be deleted."""
|
20
28
|
return self.name != "main"
|
@@ -1,8 +1,9 @@
|
|
1
1
|
"""Tenant model for CinchDB."""
|
2
2
|
|
3
3
|
from typing import Optional
|
4
|
-
from pydantic import Field
|
4
|
+
from pydantic import Field, field_validator
|
5
5
|
from .base import CinchDBBaseModel
|
6
|
+
from ..utils.name_validator import validate_name
|
6
7
|
|
7
8
|
|
8
9
|
class Tenant(CinchDBBaseModel):
|
@@ -14,6 +15,13 @@ class Tenant(CinchDBBaseModel):
|
|
14
15
|
description: Optional[str] = Field(default=None, description="Tenant description")
|
15
16
|
is_main: bool = Field(default=False, description="Whether this is the main tenant")
|
16
17
|
|
18
|
+
@field_validator('name')
|
19
|
+
@classmethod
|
20
|
+
def validate_name_field(cls, v: str) -> str:
|
21
|
+
"""Validate tenant name meets naming requirements."""
|
22
|
+
validate_name(v, "tenant")
|
23
|
+
return v
|
24
|
+
|
17
25
|
def can_delete(self) -> bool:
|
18
26
|
"""Check if this tenant can be deleted."""
|
19
27
|
return self.name != "main" and not self.is_main
|
@@ -6,10 +6,20 @@ from cinchdb.utils.sql_validator import (
|
|
6
6
|
SQLValidationError,
|
7
7
|
SQLOperation
|
8
8
|
)
|
9
|
+
from cinchdb.utils.name_validator import (
|
10
|
+
validate_name,
|
11
|
+
clean_name,
|
12
|
+
is_valid_name,
|
13
|
+
InvalidNameError
|
14
|
+
)
|
9
15
|
|
10
16
|
__all__ = [
|
11
17
|
"validate_sql_query",
|
12
18
|
"validate_query_safe",
|
13
19
|
"SQLValidationError",
|
14
|
-
"SQLOperation"
|
20
|
+
"SQLOperation",
|
21
|
+
"validate_name",
|
22
|
+
"clean_name",
|
23
|
+
"is_valid_name",
|
24
|
+
"InvalidNameError"
|
15
25
|
]
|
@@ -0,0 +1,129 @@
|
|
1
|
+
"""Name validation utilities for CinchDB entities.
|
2
|
+
|
3
|
+
Ensures branch, database, and tenant names are safe for filesystem operations
|
4
|
+
and follow consistent naming conventions.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import re
|
8
|
+
from typing import Optional
|
9
|
+
|
10
|
+
|
11
|
+
# Regex pattern for valid names: lowercase letters, numbers, dash, underscore, period
|
12
|
+
VALID_NAME_PATTERN = re.compile(r'^[a-z0-9][a-z0-9\-._]*[a-z0-9]$|^[a-z0-9]$')
|
13
|
+
|
14
|
+
# Reserved names that cannot be used
|
15
|
+
RESERVED_NAMES = {'con', 'prn', 'aux', 'nul', 'com1', 'com2', 'com3', 'com4',
|
16
|
+
'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2',
|
17
|
+
'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9'}
|
18
|
+
|
19
|
+
|
20
|
+
class InvalidNameError(ValueError):
|
21
|
+
"""Raised when a name doesn't meet validation requirements."""
|
22
|
+
pass
|
23
|
+
|
24
|
+
|
25
|
+
def validate_name(name: str, entity_type: str = "entity") -> None:
|
26
|
+
"""Validate that a name meets CinchDB naming requirements.
|
27
|
+
|
28
|
+
Valid names must:
|
29
|
+
- Contain only lowercase letters (a-z), numbers (0-9), dash (-), underscore (_), and period (.)
|
30
|
+
- Start and end with alphanumeric characters
|
31
|
+
- Be at least 1 character long
|
32
|
+
- Not exceed 255 characters (filesystem limit)
|
33
|
+
- Not be a reserved name
|
34
|
+
|
35
|
+
Args:
|
36
|
+
name: The name to validate
|
37
|
+
entity_type: Type of entity (branch, database, tenant) for error messages
|
38
|
+
|
39
|
+
Raises:
|
40
|
+
InvalidNameError: If the name is invalid
|
41
|
+
"""
|
42
|
+
if not name:
|
43
|
+
raise InvalidNameError(f"{entity_type.capitalize()} name cannot be empty")
|
44
|
+
|
45
|
+
if len(name) > 255:
|
46
|
+
raise InvalidNameError(
|
47
|
+
f"{entity_type.capitalize()} name cannot exceed 255 characters"
|
48
|
+
)
|
49
|
+
|
50
|
+
# Check for lowercase requirement
|
51
|
+
if name != name.lower():
|
52
|
+
raise InvalidNameError(
|
53
|
+
f"{entity_type.capitalize()} name must be lowercase. "
|
54
|
+
f"Use '{name.lower()}' instead of '{name}'"
|
55
|
+
)
|
56
|
+
|
57
|
+
# Check pattern
|
58
|
+
if not VALID_NAME_PATTERN.match(name):
|
59
|
+
raise InvalidNameError(
|
60
|
+
f"Invalid {entity_type} name '{name}'. "
|
61
|
+
f"Names must contain only lowercase letters (a-z), numbers (0-9), "
|
62
|
+
f"dash (-), underscore (_), and period (.). "
|
63
|
+
f"Names must start and end with alphanumeric characters."
|
64
|
+
)
|
65
|
+
|
66
|
+
# Check for consecutive special characters
|
67
|
+
if '..' in name or '--' in name or '__' in name or '.-' in name or '-.' in name:
|
68
|
+
raise InvalidNameError(
|
69
|
+
f"Invalid {entity_type} name '{name}'. "
|
70
|
+
f"Names cannot contain consecutive special characters."
|
71
|
+
)
|
72
|
+
|
73
|
+
# Check reserved names
|
74
|
+
if name.lower() in RESERVED_NAMES:
|
75
|
+
raise InvalidNameError(
|
76
|
+
f"'{name}' is a reserved name and cannot be used as a {entity_type} name"
|
77
|
+
)
|
78
|
+
|
79
|
+
|
80
|
+
def clean_name(name: str) -> str:
|
81
|
+
"""Clean a name to make it valid if possible.
|
82
|
+
|
83
|
+
This performs basic cleaning:
|
84
|
+
- Convert to lowercase
|
85
|
+
- Replace spaces with dashes
|
86
|
+
- Remove invalid characters
|
87
|
+
|
88
|
+
Args:
|
89
|
+
name: The name to clean
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
Cleaned name
|
93
|
+
|
94
|
+
Note:
|
95
|
+
This is a best-effort cleaning. The result should still be validated
|
96
|
+
with validate_name() before use.
|
97
|
+
"""
|
98
|
+
# Convert to lowercase
|
99
|
+
cleaned = name.lower()
|
100
|
+
|
101
|
+
# Replace spaces with dashes
|
102
|
+
cleaned = cleaned.replace(' ', '-')
|
103
|
+
|
104
|
+
# Remove invalid characters
|
105
|
+
cleaned = re.sub(r'[^a-z0-9\-._]', '', cleaned)
|
106
|
+
|
107
|
+
# Remove consecutive special characters
|
108
|
+
cleaned = re.sub(r'[-._]{2,}', '-', cleaned)
|
109
|
+
|
110
|
+
# Remove leading/trailing special characters
|
111
|
+
cleaned = cleaned.strip('-._')
|
112
|
+
|
113
|
+
return cleaned
|
114
|
+
|
115
|
+
|
116
|
+
def is_valid_name(name: str) -> bool:
|
117
|
+
"""Check if a name is valid without raising an exception.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
name: The name to check
|
121
|
+
|
122
|
+
Returns:
|
123
|
+
True if valid, False otherwise
|
124
|
+
"""
|
125
|
+
try:
|
126
|
+
validate_name(name)
|
127
|
+
return True
|
128
|
+
except InvalidNameError:
|
129
|
+
return False
|