cinchdb 0.1.0__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/__init__.py +7 -0
- cinchdb/__main__.py +6 -0
- cinchdb/api/__init__.py +5 -0
- cinchdb/api/app.py +76 -0
- cinchdb/api/auth.py +290 -0
- cinchdb/api/main.py +137 -0
- cinchdb/api/routers/__init__.py +25 -0
- cinchdb/api/routers/auth.py +135 -0
- cinchdb/api/routers/branches.py +368 -0
- cinchdb/api/routers/codegen.py +164 -0
- cinchdb/api/routers/columns.py +290 -0
- cinchdb/api/routers/data.py +479 -0
- cinchdb/api/routers/databases.py +177 -0
- cinchdb/api/routers/projects.py +133 -0
- cinchdb/api/routers/query.py +156 -0
- cinchdb/api/routers/tables.py +349 -0
- cinchdb/api/routers/tenants.py +216 -0
- cinchdb/api/routers/views.py +219 -0
- cinchdb/cli/__init__.py +0 -0
- cinchdb/cli/commands/__init__.py +1 -0
- cinchdb/cli/commands/branch.py +479 -0
- cinchdb/cli/commands/codegen.py +176 -0
- cinchdb/cli/commands/column.py +308 -0
- cinchdb/cli/commands/database.py +212 -0
- cinchdb/cli/commands/query.py +136 -0
- cinchdb/cli/commands/remote.py +144 -0
- cinchdb/cli/commands/table.py +289 -0
- cinchdb/cli/commands/tenant.py +173 -0
- cinchdb/cli/commands/view.py +189 -0
- cinchdb/cli/handlers/__init__.py +5 -0
- cinchdb/cli/handlers/codegen_handler.py +189 -0
- cinchdb/cli/main.py +137 -0
- cinchdb/cli/utils.py +182 -0
- cinchdb/config.py +177 -0
- cinchdb/core/__init__.py +5 -0
- cinchdb/core/connection.py +175 -0
- cinchdb/core/database.py +537 -0
- cinchdb/core/maintenance.py +73 -0
- cinchdb/core/path_utils.py +153 -0
- cinchdb/managers/__init__.py +26 -0
- cinchdb/managers/branch.py +167 -0
- cinchdb/managers/change_applier.py +414 -0
- cinchdb/managers/change_comparator.py +194 -0
- cinchdb/managers/change_tracker.py +182 -0
- cinchdb/managers/codegen.py +523 -0
- cinchdb/managers/column.py +579 -0
- cinchdb/managers/data.py +455 -0
- cinchdb/managers/merge_manager.py +429 -0
- cinchdb/managers/query.py +214 -0
- cinchdb/managers/table.py +383 -0
- cinchdb/managers/tenant.py +258 -0
- cinchdb/managers/view.py +252 -0
- cinchdb/models/__init__.py +27 -0
- cinchdb/models/base.py +44 -0
- cinchdb/models/branch.py +26 -0
- cinchdb/models/change.py +47 -0
- cinchdb/models/database.py +20 -0
- cinchdb/models/project.py +20 -0
- cinchdb/models/table.py +86 -0
- cinchdb/models/tenant.py +19 -0
- cinchdb/models/view.py +15 -0
- cinchdb/utils/__init__.py +15 -0
- cinchdb/utils/sql_validator.py +137 -0
- cinchdb-0.1.0.dist-info/METADATA +195 -0
- cinchdb-0.1.0.dist-info/RECORD +68 -0
- cinchdb-0.1.0.dist-info/WHEEL +4 -0
- cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
- cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
"""Projects router for CinchDB API."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Optional
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
from cinchdb.config import Config
|
9
|
+
from cinchdb.api.auth import (
|
10
|
+
AuthContext,
|
11
|
+
require_write_permission,
|
12
|
+
require_read_permission,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
router = APIRouter()
|
17
|
+
|
18
|
+
|
19
|
+
class ProjectInfo(BaseModel):
|
20
|
+
"""Project information response."""
|
21
|
+
|
22
|
+
path: str
|
23
|
+
active_database: str
|
24
|
+
active_branch: str
|
25
|
+
has_api_keys: bool
|
26
|
+
|
27
|
+
|
28
|
+
class InitProjectRequest(BaseModel):
|
29
|
+
"""Request to initialize a new project."""
|
30
|
+
|
31
|
+
path: str
|
32
|
+
|
33
|
+
|
34
|
+
@router.post("/init")
|
35
|
+
async def init_project(
|
36
|
+
request: InitProjectRequest, auth: AuthContext = Depends(require_write_permission)
|
37
|
+
):
|
38
|
+
"""Initialize a new CinchDB project."""
|
39
|
+
project_path = Path(request.path)
|
40
|
+
|
41
|
+
# Check if directory exists
|
42
|
+
if not project_path.exists():
|
43
|
+
try:
|
44
|
+
project_path.mkdir(parents=True)
|
45
|
+
except Exception as e:
|
46
|
+
raise HTTPException(
|
47
|
+
status_code=400, detail=f"Failed to create directory: {e}"
|
48
|
+
)
|
49
|
+
|
50
|
+
# Initialize project
|
51
|
+
try:
|
52
|
+
config = Config(project_path)
|
53
|
+
config.init_project()
|
54
|
+
return {"message": f"Initialized CinchDB project in {project_path}"}
|
55
|
+
except FileExistsError:
|
56
|
+
raise HTTPException(status_code=400, detail="Project already exists")
|
57
|
+
except Exception as e:
|
58
|
+
raise HTTPException(
|
59
|
+
status_code=500, detail=f"Failed to initialize project: {e}"
|
60
|
+
)
|
61
|
+
|
62
|
+
|
63
|
+
@router.get("/info")
|
64
|
+
async def get_project_info(
|
65
|
+
auth: AuthContext = Depends(require_read_permission),
|
66
|
+
) -> ProjectInfo:
|
67
|
+
"""Get information about the current project."""
|
68
|
+
config = Config(auth.project_dir)
|
69
|
+
|
70
|
+
try:
|
71
|
+
config_data = config.load()
|
72
|
+
has_keys = bool(config_data.api_keys)
|
73
|
+
|
74
|
+
return ProjectInfo(
|
75
|
+
path=str(auth.project_dir),
|
76
|
+
active_database=config_data.active_database,
|
77
|
+
active_branch=config_data.active_branch,
|
78
|
+
has_api_keys=has_keys,
|
79
|
+
)
|
80
|
+
except FileNotFoundError:
|
81
|
+
raise HTTPException(status_code=404, detail="Project configuration not found")
|
82
|
+
|
83
|
+
|
84
|
+
class SetActiveRequest(BaseModel):
|
85
|
+
"""Request to set active database or branch."""
|
86
|
+
|
87
|
+
database: Optional[str] = None
|
88
|
+
branch: Optional[str] = None
|
89
|
+
|
90
|
+
|
91
|
+
@router.put("/active")
|
92
|
+
async def set_active(
|
93
|
+
request: SetActiveRequest, auth: AuthContext = Depends(require_write_permission)
|
94
|
+
):
|
95
|
+
"""Set the active database and/or branch."""
|
96
|
+
config = Config(auth.project_dir)
|
97
|
+
|
98
|
+
try:
|
99
|
+
config_data = config.load()
|
100
|
+
|
101
|
+
if request.database:
|
102
|
+
# Verify database exists
|
103
|
+
db_path = auth.project_dir / ".cinchdb" / "databases" / request.database
|
104
|
+
if not db_path.exists():
|
105
|
+
raise HTTPException(
|
106
|
+
status_code=404, detail=f"Database '{request.database}' not found"
|
107
|
+
)
|
108
|
+
config_data.active_database = request.database
|
109
|
+
|
110
|
+
if request.branch:
|
111
|
+
# Verify branch exists in active database
|
112
|
+
branch_path = (
|
113
|
+
auth.project_dir
|
114
|
+
/ ".cinchdb"
|
115
|
+
/ "databases"
|
116
|
+
/ config_data.active_database
|
117
|
+
/ "branches"
|
118
|
+
/ request.branch
|
119
|
+
)
|
120
|
+
if not branch_path.exists():
|
121
|
+
raise HTTPException(
|
122
|
+
status_code=404, detail=f"Branch '{request.branch}' not found"
|
123
|
+
)
|
124
|
+
config_data.active_branch = request.branch
|
125
|
+
|
126
|
+
config.save(config_data)
|
127
|
+
|
128
|
+
return {
|
129
|
+
"active_database": config_data.active_database,
|
130
|
+
"active_branch": config_data.active_branch,
|
131
|
+
}
|
132
|
+
except FileNotFoundError:
|
133
|
+
raise HTTPException(status_code=404, detail="Project configuration not found")
|
@@ -0,0 +1,156 @@
|
|
1
|
+
"""Query router for CinchDB API."""
|
2
|
+
|
3
|
+
from typing import List, Dict, Any, Optional
|
4
|
+
from fastapi import APIRouter, Depends, HTTPException, Query as QueryParam
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from cinchdb.core.path_utils import get_tenant_db_path
|
8
|
+
from cinchdb.core.connection import DatabaseConnection
|
9
|
+
from cinchdb.api.auth import (
|
10
|
+
AuthContext,
|
11
|
+
require_read_permission,
|
12
|
+
require_write_permission,
|
13
|
+
)
|
14
|
+
from cinchdb.utils import validate_sql_query, SQLValidationError, SQLOperation
|
15
|
+
|
16
|
+
|
17
|
+
router = APIRouter()
|
18
|
+
|
19
|
+
|
20
|
+
class QueryRequest(BaseModel):
|
21
|
+
"""Request to execute a query."""
|
22
|
+
|
23
|
+
sql: str
|
24
|
+
limit: Optional[int] = None
|
25
|
+
|
26
|
+
|
27
|
+
class QueryResult(BaseModel):
|
28
|
+
"""Query execution result."""
|
29
|
+
|
30
|
+
columns: List[str]
|
31
|
+
rows: List[List[Any]]
|
32
|
+
row_count: int
|
33
|
+
affected_rows: Optional[int] = None
|
34
|
+
|
35
|
+
|
36
|
+
@router.post("/execute")
|
37
|
+
async def execute_query(
|
38
|
+
request: QueryRequest,
|
39
|
+
database: str = QueryParam(..., description="Database name"),
|
40
|
+
branch: str = QueryParam(..., description="Branch name"),
|
41
|
+
tenant: str = QueryParam(..., description="Tenant name"),
|
42
|
+
auth: AuthContext = Depends(require_read_permission),
|
43
|
+
) -> QueryResult:
|
44
|
+
"""Execute a SQL query.
|
45
|
+
|
46
|
+
SELECT queries require read permission.
|
47
|
+
INSERT/UPDATE/DELETE queries require write permission.
|
48
|
+
"""
|
49
|
+
db_name = database
|
50
|
+
branch_name = branch
|
51
|
+
|
52
|
+
# Validate the SQL query first
|
53
|
+
try:
|
54
|
+
is_valid, error_msg, operation = validate_sql_query(request.sql)
|
55
|
+
if not is_valid:
|
56
|
+
raise HTTPException(status_code=400, detail=error_msg)
|
57
|
+
except Exception as e:
|
58
|
+
raise HTTPException(status_code=400, detail=str(e))
|
59
|
+
|
60
|
+
# Check if this is a write operation based on validated operation
|
61
|
+
sql_upper = request.sql.strip().upper()
|
62
|
+
is_write = operation in (SQLOperation.INSERT, SQLOperation.UPDATE, SQLOperation.DELETE)
|
63
|
+
|
64
|
+
if is_write:
|
65
|
+
# Require write permission for write operations
|
66
|
+
await require_write_permission(auth, branch_name)
|
67
|
+
else:
|
68
|
+
# Check branch permissions for read
|
69
|
+
await require_read_permission(auth, branch_name)
|
70
|
+
|
71
|
+
# Add LIMIT if specified and not already present
|
72
|
+
query_sql = request.sql
|
73
|
+
if request.limit and "LIMIT" not in sql_upper:
|
74
|
+
query_sql = f"{request.sql} LIMIT {request.limit}"
|
75
|
+
|
76
|
+
# Get database path
|
77
|
+
db_path = get_tenant_db_path(auth.project_dir, db_name, branch_name, tenant)
|
78
|
+
|
79
|
+
try:
|
80
|
+
with DatabaseConnection(db_path) as conn:
|
81
|
+
cursor = conn.execute(query_sql)
|
82
|
+
|
83
|
+
# Check if this is a SELECT query
|
84
|
+
is_select = sql_upper.startswith("SELECT")
|
85
|
+
|
86
|
+
if is_select:
|
87
|
+
rows = cursor.fetchall()
|
88
|
+
|
89
|
+
# Get column names
|
90
|
+
columns = (
|
91
|
+
[desc[0] for desc in cursor.description]
|
92
|
+
if cursor.description
|
93
|
+
else []
|
94
|
+
)
|
95
|
+
|
96
|
+
# Convert rows to lists (from sqlite3.Row objects)
|
97
|
+
row_data = []
|
98
|
+
for row in rows:
|
99
|
+
row_data.append(list(row))
|
100
|
+
|
101
|
+
return QueryResult(
|
102
|
+
columns=columns,
|
103
|
+
rows=row_data,
|
104
|
+
row_count=len(rows),
|
105
|
+
affected_rows=None,
|
106
|
+
)
|
107
|
+
else:
|
108
|
+
# For INSERT/UPDATE/DELETE, commit and return affected rows
|
109
|
+
conn.commit()
|
110
|
+
affected = cursor.rowcount
|
111
|
+
|
112
|
+
return QueryResult(
|
113
|
+
columns=[], rows=[], row_count=0, affected_rows=affected
|
114
|
+
)
|
115
|
+
|
116
|
+
except Exception as e:
|
117
|
+
raise HTTPException(status_code=400, detail=f"Query error: {str(e)}")
|
118
|
+
|
119
|
+
|
120
|
+
@router.get("/tables/{table}/data")
|
121
|
+
async def get_table_data(
|
122
|
+
table: str,
|
123
|
+
database: str = QueryParam(..., description="Database name"),
|
124
|
+
branch: str = QueryParam(..., description="Branch name"),
|
125
|
+
tenant: str = QueryParam(..., description="Tenant name"),
|
126
|
+
limit: int = QueryParam(100, description="Maximum rows to return"),
|
127
|
+
offset: int = QueryParam(0, description="Number of rows to skip"),
|
128
|
+
auth: AuthContext = Depends(require_read_permission),
|
129
|
+
) -> QueryResult:
|
130
|
+
"""Get data from a specific table with pagination."""
|
131
|
+
# Build query
|
132
|
+
query = QueryRequest(
|
133
|
+
sql=f"SELECT * FROM {table} LIMIT {limit} OFFSET {offset}",
|
134
|
+
limit=None, # Already included in SQL
|
135
|
+
)
|
136
|
+
|
137
|
+
return await execute_query(query, database, branch, tenant, auth)
|
138
|
+
|
139
|
+
|
140
|
+
@router.get("/tables/{table}/count")
|
141
|
+
async def get_table_count(
|
142
|
+
table: str,
|
143
|
+
database: str = QueryParam(..., description="Database name"),
|
144
|
+
branch: str = QueryParam(..., description="Branch name"),
|
145
|
+
tenant: str = QueryParam(..., description="Tenant name"),
|
146
|
+
auth: AuthContext = Depends(require_read_permission),
|
147
|
+
) -> Dict[str, int]:
|
148
|
+
"""Get the row count for a table."""
|
149
|
+
# Build query
|
150
|
+
query = QueryRequest(sql=f"SELECT COUNT(*) as count FROM {table}", limit=None)
|
151
|
+
|
152
|
+
result = await execute_query(query, database, branch, tenant, auth)
|
153
|
+
|
154
|
+
if result.rows and result.rows[0]:
|
155
|
+
return {"count": result.rows[0][0]}
|
156
|
+
return {"count": 0}
|
@@ -0,0 +1,349 @@
|
|
1
|
+
"""Tables router for CinchDB API."""
|
2
|
+
|
3
|
+
from typing import List, Optional
|
4
|
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from cinchdb.core.database import CinchDB
|
8
|
+
from cinchdb.managers.change_applier import ChangeApplier
|
9
|
+
from cinchdb.models import Column, ForeignKeyRef
|
10
|
+
from cinchdb.api.auth import (
|
11
|
+
AuthContext,
|
12
|
+
require_write_permission,
|
13
|
+
require_read_permission,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
router = APIRouter()
|
18
|
+
|
19
|
+
|
20
|
+
class ForeignKeySchema(BaseModel):
|
21
|
+
"""Foreign key schema for requests."""
|
22
|
+
|
23
|
+
table: str
|
24
|
+
column: str = "id"
|
25
|
+
on_delete: str = "RESTRICT"
|
26
|
+
on_update: str = "RESTRICT"
|
27
|
+
|
28
|
+
|
29
|
+
class ColumnSchema(BaseModel):
|
30
|
+
"""Column schema for requests."""
|
31
|
+
|
32
|
+
name: str
|
33
|
+
type: str
|
34
|
+
nullable: bool = False
|
35
|
+
default: Optional[str] = None
|
36
|
+
primary_key: bool = False
|
37
|
+
unique: bool = False
|
38
|
+
foreign_key: Optional[ForeignKeySchema] = None
|
39
|
+
|
40
|
+
|
41
|
+
class TableInfo(BaseModel):
|
42
|
+
"""Table information."""
|
43
|
+
|
44
|
+
name: str
|
45
|
+
column_count: int
|
46
|
+
columns: List[ColumnSchema]
|
47
|
+
|
48
|
+
|
49
|
+
class CreateTableRequest(BaseModel):
|
50
|
+
"""Request to create a table."""
|
51
|
+
|
52
|
+
name: str
|
53
|
+
columns: List[ColumnSchema]
|
54
|
+
|
55
|
+
|
56
|
+
class CopyTableRequest(BaseModel):
|
57
|
+
"""Request to copy a table."""
|
58
|
+
|
59
|
+
source: str
|
60
|
+
target: str
|
61
|
+
copy_data: bool = True
|
62
|
+
|
63
|
+
|
64
|
+
@router.get("/", response_model=List[TableInfo])
|
65
|
+
async def list_tables(
|
66
|
+
database: str = Query(..., description="Database name"),
|
67
|
+
branch: str = Query(..., description="Branch name"),
|
68
|
+
auth: AuthContext = Depends(require_read_permission),
|
69
|
+
):
|
70
|
+
"""List all tables in a branch."""
|
71
|
+
db_name = database
|
72
|
+
branch_name = branch
|
73
|
+
|
74
|
+
# Check branch permissions
|
75
|
+
await require_read_permission(auth, branch_name)
|
76
|
+
|
77
|
+
try:
|
78
|
+
db = CinchDB(
|
79
|
+
database=db_name,
|
80
|
+
branch=branch_name,
|
81
|
+
tenant="main",
|
82
|
+
project_dir=auth.project_dir,
|
83
|
+
)
|
84
|
+
tables = db.tables.list_tables()
|
85
|
+
|
86
|
+
result = []
|
87
|
+
for table in tables:
|
88
|
+
# Convert columns
|
89
|
+
columns = []
|
90
|
+
for col in table.columns:
|
91
|
+
# Convert foreign key if present
|
92
|
+
fk_schema = None
|
93
|
+
if col.foreign_key:
|
94
|
+
fk_schema = ForeignKeySchema(
|
95
|
+
table=col.foreign_key.table,
|
96
|
+
column=col.foreign_key.column,
|
97
|
+
on_delete=col.foreign_key.on_delete,
|
98
|
+
on_update=col.foreign_key.on_update,
|
99
|
+
)
|
100
|
+
|
101
|
+
columns.append(
|
102
|
+
ColumnSchema(
|
103
|
+
name=col.name,
|
104
|
+
type=col.type,
|
105
|
+
nullable=col.nullable,
|
106
|
+
default=col.default,
|
107
|
+
primary_key=col.primary_key,
|
108
|
+
unique=col.unique,
|
109
|
+
foreign_key=fk_schema,
|
110
|
+
)
|
111
|
+
)
|
112
|
+
|
113
|
+
# Count user-defined columns
|
114
|
+
user_columns = [
|
115
|
+
c
|
116
|
+
for c in table.columns
|
117
|
+
if c.name not in ["id", "created_at", "updated_at"]
|
118
|
+
]
|
119
|
+
|
120
|
+
result.append(
|
121
|
+
TableInfo(
|
122
|
+
name=table.name, column_count=len(user_columns), columns=columns
|
123
|
+
)
|
124
|
+
)
|
125
|
+
|
126
|
+
return result
|
127
|
+
|
128
|
+
except ValueError as e:
|
129
|
+
raise HTTPException(status_code=404, detail=str(e))
|
130
|
+
|
131
|
+
|
132
|
+
@router.post("/")
|
133
|
+
async def create_table(
|
134
|
+
request: CreateTableRequest,
|
135
|
+
database: str = Query(..., description="Database name"),
|
136
|
+
branch: str = Query(..., description="Branch name"),
|
137
|
+
apply: bool = Query(True, description="Apply changes to all tenants"),
|
138
|
+
auth: AuthContext = Depends(require_write_permission),
|
139
|
+
):
|
140
|
+
"""Create a new table."""
|
141
|
+
db_name = database
|
142
|
+
branch_name = branch
|
143
|
+
|
144
|
+
# Check branch permissions
|
145
|
+
await require_write_permission(auth, branch_name)
|
146
|
+
|
147
|
+
# Convert columns
|
148
|
+
columns = []
|
149
|
+
for col_schema in request.columns:
|
150
|
+
# Validate type
|
151
|
+
if col_schema.type.upper() not in [
|
152
|
+
"TEXT",
|
153
|
+
"INTEGER",
|
154
|
+
"REAL",
|
155
|
+
"BLOB",
|
156
|
+
"NUMERIC",
|
157
|
+
]:
|
158
|
+
raise HTTPException(
|
159
|
+
status_code=400, detail=f"Invalid column type: {col_schema.type}"
|
160
|
+
)
|
161
|
+
|
162
|
+
# Convert foreign key if present
|
163
|
+
fk_ref = None
|
164
|
+
if col_schema.foreign_key:
|
165
|
+
# Validate FK action
|
166
|
+
if col_schema.foreign_key.on_delete not in ["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]:
|
167
|
+
raise HTTPException(
|
168
|
+
status_code=400,
|
169
|
+
detail=f"Invalid on_delete action: {col_schema.foreign_key.on_delete}"
|
170
|
+
)
|
171
|
+
if col_schema.foreign_key.on_update not in ["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]:
|
172
|
+
raise HTTPException(
|
173
|
+
status_code=400,
|
174
|
+
detail=f"Invalid on_update action: {col_schema.foreign_key.on_update}"
|
175
|
+
)
|
176
|
+
|
177
|
+
fk_ref = ForeignKeyRef(
|
178
|
+
table=col_schema.foreign_key.table,
|
179
|
+
column=col_schema.foreign_key.column,
|
180
|
+
on_delete=col_schema.foreign_key.on_delete,
|
181
|
+
on_update=col_schema.foreign_key.on_update,
|
182
|
+
)
|
183
|
+
|
184
|
+
columns.append(
|
185
|
+
Column(
|
186
|
+
name=col_schema.name,
|
187
|
+
type=col_schema.type.upper(),
|
188
|
+
nullable=col_schema.nullable,
|
189
|
+
default=col_schema.default,
|
190
|
+
primary_key=col_schema.primary_key,
|
191
|
+
unique=col_schema.unique,
|
192
|
+
foreign_key=fk_ref,
|
193
|
+
)
|
194
|
+
)
|
195
|
+
|
196
|
+
try:
|
197
|
+
db = CinchDB(
|
198
|
+
database=db_name,
|
199
|
+
branch=branch_name,
|
200
|
+
tenant="main",
|
201
|
+
project_dir=auth.project_dir,
|
202
|
+
)
|
203
|
+
db.tables.create_table(request.name, columns)
|
204
|
+
|
205
|
+
# Apply to all tenants if requested
|
206
|
+
if apply:
|
207
|
+
applier = ChangeApplier(auth.project_dir, db_name, branch_name)
|
208
|
+
applier.apply_all_unapplied()
|
209
|
+
|
210
|
+
return {
|
211
|
+
"message": f"Created table '{request.name}' with {len(columns)} columns"
|
212
|
+
}
|
213
|
+
|
214
|
+
except ValueError as e:
|
215
|
+
raise HTTPException(status_code=400, detail=str(e))
|
216
|
+
|
217
|
+
|
218
|
+
@router.delete("/{name}")
|
219
|
+
async def delete_table(
|
220
|
+
name: str,
|
221
|
+
database: str = Query(..., description="Database name"),
|
222
|
+
branch: str = Query(..., description="Branch name"),
|
223
|
+
apply: bool = Query(True, description="Apply changes to all tenants"),
|
224
|
+
auth: AuthContext = Depends(require_write_permission),
|
225
|
+
):
|
226
|
+
"""Delete a table."""
|
227
|
+
db_name = database
|
228
|
+
branch_name = branch
|
229
|
+
|
230
|
+
# Check branch permissions
|
231
|
+
await require_write_permission(auth, branch_name)
|
232
|
+
|
233
|
+
try:
|
234
|
+
db = CinchDB(
|
235
|
+
database=db_name,
|
236
|
+
branch=branch_name,
|
237
|
+
tenant="main",
|
238
|
+
project_dir=auth.project_dir,
|
239
|
+
)
|
240
|
+
db.tables.delete_table(name)
|
241
|
+
|
242
|
+
# Apply to all tenants if requested
|
243
|
+
if apply:
|
244
|
+
applier = ChangeApplier(auth.project_dir, db_name, branch_name)
|
245
|
+
applier.apply_all_unapplied()
|
246
|
+
|
247
|
+
return {"message": f"Deleted table '{name}'"}
|
248
|
+
|
249
|
+
except ValueError as e:
|
250
|
+
raise HTTPException(status_code=404, detail=str(e))
|
251
|
+
|
252
|
+
|
253
|
+
@router.post("/copy")
|
254
|
+
async def copy_table(
|
255
|
+
request: CopyTableRequest,
|
256
|
+
database: str = Query(..., description="Database name"),
|
257
|
+
branch: str = Query(..., description="Branch name"),
|
258
|
+
apply: bool = Query(True, description="Apply changes to all tenants"),
|
259
|
+
auth: AuthContext = Depends(require_write_permission),
|
260
|
+
):
|
261
|
+
"""Copy a table to a new table."""
|
262
|
+
db_name = database
|
263
|
+
branch_name = branch
|
264
|
+
|
265
|
+
# Check branch permissions
|
266
|
+
await require_write_permission(auth, branch_name)
|
267
|
+
|
268
|
+
try:
|
269
|
+
db = CinchDB(
|
270
|
+
database=db_name,
|
271
|
+
branch=branch_name,
|
272
|
+
tenant="main",
|
273
|
+
project_dir=auth.project_dir,
|
274
|
+
)
|
275
|
+
db.tables.copy_table(request.source, request.target, request.copy_data)
|
276
|
+
|
277
|
+
# Apply to all tenants if requested
|
278
|
+
if apply:
|
279
|
+
applier = ChangeApplier(auth.project_dir, db_name, branch_name)
|
280
|
+
applier.apply_all_unapplied()
|
281
|
+
|
282
|
+
data_msg = "with data" if request.copy_data else "without data"
|
283
|
+
return {
|
284
|
+
"message": f"Copied table '{request.source}' to '{request.target}' {data_msg}"
|
285
|
+
}
|
286
|
+
|
287
|
+
except ValueError as e:
|
288
|
+
raise HTTPException(status_code=400, detail=str(e))
|
289
|
+
|
290
|
+
|
291
|
+
@router.get("/{name}")
|
292
|
+
async def get_table_info(
|
293
|
+
name: str,
|
294
|
+
database: str = Query(..., description="Database name"),
|
295
|
+
branch: str = Query(..., description="Branch name"),
|
296
|
+
auth: AuthContext = Depends(require_read_permission),
|
297
|
+
) -> TableInfo:
|
298
|
+
"""Get information about a specific table."""
|
299
|
+
db_name = database
|
300
|
+
branch_name = branch
|
301
|
+
|
302
|
+
# Check branch permissions
|
303
|
+
await require_read_permission(auth, branch_name)
|
304
|
+
|
305
|
+
try:
|
306
|
+
db = CinchDB(
|
307
|
+
database=db_name,
|
308
|
+
branch=branch_name,
|
309
|
+
tenant="main",
|
310
|
+
project_dir=auth.project_dir,
|
311
|
+
)
|
312
|
+
table = db.tables.get_table(name)
|
313
|
+
|
314
|
+
# Convert columns
|
315
|
+
columns = []
|
316
|
+
for col in table.columns:
|
317
|
+
# Convert foreign key if present
|
318
|
+
fk_schema = None
|
319
|
+
if col.foreign_key:
|
320
|
+
fk_schema = ForeignKeySchema(
|
321
|
+
table=col.foreign_key.table,
|
322
|
+
column=col.foreign_key.column,
|
323
|
+
on_delete=col.foreign_key.on_delete,
|
324
|
+
on_update=col.foreign_key.on_update,
|
325
|
+
)
|
326
|
+
|
327
|
+
columns.append(
|
328
|
+
ColumnSchema(
|
329
|
+
name=col.name,
|
330
|
+
type=col.type,
|
331
|
+
nullable=col.nullable,
|
332
|
+
default=col.default,
|
333
|
+
primary_key=col.primary_key,
|
334
|
+
unique=col.unique,
|
335
|
+
foreign_key=fk_schema,
|
336
|
+
)
|
337
|
+
)
|
338
|
+
|
339
|
+
# Count user-defined columns
|
340
|
+
user_columns = [
|
341
|
+
c for c in table.columns if c.name not in ["id", "created_at", "updated_at"]
|
342
|
+
]
|
343
|
+
|
344
|
+
return TableInfo(
|
345
|
+
name=table.name, column_count=len(user_columns), columns=columns
|
346
|
+
)
|
347
|
+
|
348
|
+
except ValueError as e:
|
349
|
+
raise HTTPException(status_code=404, detail=str(e))
|