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.
Files changed (68) hide show
  1. cinchdb/__init__.py +7 -0
  2. cinchdb/__main__.py +6 -0
  3. cinchdb/api/__init__.py +5 -0
  4. cinchdb/api/app.py +76 -0
  5. cinchdb/api/auth.py +290 -0
  6. cinchdb/api/main.py +137 -0
  7. cinchdb/api/routers/__init__.py +25 -0
  8. cinchdb/api/routers/auth.py +135 -0
  9. cinchdb/api/routers/branches.py +368 -0
  10. cinchdb/api/routers/codegen.py +164 -0
  11. cinchdb/api/routers/columns.py +290 -0
  12. cinchdb/api/routers/data.py +479 -0
  13. cinchdb/api/routers/databases.py +177 -0
  14. cinchdb/api/routers/projects.py +133 -0
  15. cinchdb/api/routers/query.py +156 -0
  16. cinchdb/api/routers/tables.py +349 -0
  17. cinchdb/api/routers/tenants.py +216 -0
  18. cinchdb/api/routers/views.py +219 -0
  19. cinchdb/cli/__init__.py +0 -0
  20. cinchdb/cli/commands/__init__.py +1 -0
  21. cinchdb/cli/commands/branch.py +479 -0
  22. cinchdb/cli/commands/codegen.py +176 -0
  23. cinchdb/cli/commands/column.py +308 -0
  24. cinchdb/cli/commands/database.py +212 -0
  25. cinchdb/cli/commands/query.py +136 -0
  26. cinchdb/cli/commands/remote.py +144 -0
  27. cinchdb/cli/commands/table.py +289 -0
  28. cinchdb/cli/commands/tenant.py +173 -0
  29. cinchdb/cli/commands/view.py +189 -0
  30. cinchdb/cli/handlers/__init__.py +5 -0
  31. cinchdb/cli/handlers/codegen_handler.py +189 -0
  32. cinchdb/cli/main.py +137 -0
  33. cinchdb/cli/utils.py +182 -0
  34. cinchdb/config.py +177 -0
  35. cinchdb/core/__init__.py +5 -0
  36. cinchdb/core/connection.py +175 -0
  37. cinchdb/core/database.py +537 -0
  38. cinchdb/core/maintenance.py +73 -0
  39. cinchdb/core/path_utils.py +153 -0
  40. cinchdb/managers/__init__.py +26 -0
  41. cinchdb/managers/branch.py +167 -0
  42. cinchdb/managers/change_applier.py +414 -0
  43. cinchdb/managers/change_comparator.py +194 -0
  44. cinchdb/managers/change_tracker.py +182 -0
  45. cinchdb/managers/codegen.py +523 -0
  46. cinchdb/managers/column.py +579 -0
  47. cinchdb/managers/data.py +455 -0
  48. cinchdb/managers/merge_manager.py +429 -0
  49. cinchdb/managers/query.py +214 -0
  50. cinchdb/managers/table.py +383 -0
  51. cinchdb/managers/tenant.py +258 -0
  52. cinchdb/managers/view.py +252 -0
  53. cinchdb/models/__init__.py +27 -0
  54. cinchdb/models/base.py +44 -0
  55. cinchdb/models/branch.py +26 -0
  56. cinchdb/models/change.py +47 -0
  57. cinchdb/models/database.py +20 -0
  58. cinchdb/models/project.py +20 -0
  59. cinchdb/models/table.py +86 -0
  60. cinchdb/models/tenant.py +19 -0
  61. cinchdb/models/view.py +15 -0
  62. cinchdb/utils/__init__.py +15 -0
  63. cinchdb/utils/sql_validator.py +137 -0
  64. cinchdb-0.1.0.dist-info/METADATA +195 -0
  65. cinchdb-0.1.0.dist-info/RECORD +68 -0
  66. cinchdb-0.1.0.dist-info/WHEEL +4 -0
  67. cinchdb-0.1.0.dist-info/entry_points.txt +3 -0
  68. cinchdb-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,252 @@
1
+ """View/Model management for CinchDB."""
2
+
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from cinchdb.models import View, Change, ChangeType
7
+ from cinchdb.core.connection import DatabaseConnection
8
+ from cinchdb.core.path_utils import get_tenant_db_path
9
+ from cinchdb.core.maintenance import check_maintenance_mode
10
+ from cinchdb.managers.change_tracker import ChangeTracker
11
+
12
+
13
+ class ViewModel:
14
+ """Manages SQL views (models) in the database."""
15
+
16
+ def __init__(
17
+ self, project_root: Path, database: str, branch: str, tenant: str = "main"
18
+ ):
19
+ """Initialize view manager.
20
+
21
+ Args:
22
+ project_root: Path to project root
23
+ database: Database name
24
+ branch: Branch name
25
+ tenant: Tenant name (default: main)
26
+ """
27
+ self.project_root = Path(project_root)
28
+ self.database = database
29
+ self.branch = branch
30
+ self.tenant = tenant
31
+ self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
32
+ self.change_tracker = ChangeTracker(project_root, database, branch)
33
+
34
+ def list_views(self) -> List[View]:
35
+ """List all views in the database.
36
+
37
+ Returns:
38
+ List of View objects
39
+ """
40
+ views = []
41
+
42
+ with DatabaseConnection(self.db_path) as conn:
43
+ # Get all views
44
+ cursor = conn.execute(
45
+ """
46
+ SELECT name, sql FROM sqlite_master
47
+ WHERE type='view'
48
+ ORDER BY name
49
+ """
50
+ )
51
+
52
+ for row in cursor.fetchall():
53
+ view = View(
54
+ name=row["name"],
55
+ database=self.database,
56
+ branch=self.branch,
57
+ sql_statement=row["sql"],
58
+ )
59
+ views.append(view)
60
+
61
+ return views
62
+
63
+ def create_view(self, view_name: str, sql_statement: str) -> View:
64
+ """Create a new view.
65
+
66
+ Args:
67
+ view_name: Name of the view
68
+ sql_statement: SQL SELECT statement defining the view
69
+
70
+ Returns:
71
+ Created View object
72
+
73
+ Raises:
74
+ ValueError: If view already exists
75
+ MaintenanceError: If branch is in maintenance mode
76
+ """
77
+ # Check maintenance mode
78
+ check_maintenance_mode(self.project_root, self.database, self.branch)
79
+
80
+ # Check if view already exists
81
+ if self._view_exists(view_name):
82
+ raise ValueError(f"View '{view_name}' already exists")
83
+
84
+ # Create the view
85
+ create_sql = f"CREATE VIEW {view_name} AS {sql_statement}"
86
+
87
+ # Track the change
88
+ change = Change(
89
+ type=ChangeType.CREATE_VIEW,
90
+ entity_type="view",
91
+ entity_name=view_name,
92
+ branch=self.branch,
93
+ details={"sql_statement": sql_statement},
94
+ sql=create_sql,
95
+ )
96
+ self.change_tracker.add_change(change)
97
+
98
+ # Apply to all tenants in the branch
99
+ from cinchdb.managers.change_applier import ChangeApplier
100
+
101
+ applier = ChangeApplier(self.project_root, self.database, self.branch)
102
+ applier.apply_change(change.id)
103
+
104
+ # Return the created view
105
+ return View(
106
+ name=view_name,
107
+ database=self.database,
108
+ branch=self.branch,
109
+ sql_statement=sql_statement,
110
+ )
111
+
112
+ def update_view(self, view_name: str, sql_statement: str) -> View:
113
+ """Update an existing view's SQL.
114
+
115
+ Args:
116
+ view_name: Name of the view to update
117
+ sql_statement: New SQL SELECT statement
118
+
119
+ Returns:
120
+ Updated View object
121
+
122
+ Raises:
123
+ ValueError: If view doesn't exist
124
+ MaintenanceError: If branch is in maintenance mode
125
+ """
126
+ # Check maintenance mode
127
+ check_maintenance_mode(self.project_root, self.database, self.branch)
128
+
129
+ # Check if view exists
130
+ if not self._view_exists(view_name):
131
+ raise ValueError(f"View '{view_name}' does not exist")
132
+
133
+ # SQLite doesn't support CREATE OR REPLACE VIEW, so we need to drop and recreate
134
+ create_sql = f"CREATE VIEW {view_name} AS {sql_statement}"
135
+
136
+ # Track the change
137
+ change = Change(
138
+ type=ChangeType.UPDATE_VIEW,
139
+ entity_type="view",
140
+ entity_name=view_name,
141
+ branch=self.branch,
142
+ details={
143
+ "sql_statement": sql_statement,
144
+ "drop_sql": f"DROP VIEW {view_name}",
145
+ },
146
+ sql=create_sql,
147
+ )
148
+ self.change_tracker.add_change(change)
149
+
150
+ # Apply to all tenants in the branch
151
+ from cinchdb.managers.change_applier import ChangeApplier
152
+
153
+ applier = ChangeApplier(self.project_root, self.database, self.branch)
154
+ applier.apply_change(change.id)
155
+
156
+ # Return the updated view
157
+ return View(
158
+ name=view_name,
159
+ database=self.database,
160
+ branch=self.branch,
161
+ sql_statement=sql_statement,
162
+ )
163
+
164
+ def delete_view(self, view_name: str) -> None:
165
+ """Delete a view.
166
+
167
+ Args:
168
+ view_name: Name of the view to delete
169
+
170
+ Raises:
171
+ ValueError: If view doesn't exist
172
+ MaintenanceError: If branch is in maintenance mode
173
+ """
174
+ # Check maintenance mode
175
+ check_maintenance_mode(self.project_root, self.database, self.branch)
176
+
177
+ # Check if view exists
178
+ if not self._view_exists(view_name):
179
+ raise ValueError(f"View '{view_name}' does not exist")
180
+
181
+ # Drop the view
182
+ drop_sql = f"DROP VIEW {view_name}"
183
+
184
+ # Track the change
185
+ change = Change(
186
+ type=ChangeType.DROP_VIEW,
187
+ entity_type="view",
188
+ entity_name=view_name,
189
+ branch=self.branch,
190
+ sql=drop_sql,
191
+ )
192
+ self.change_tracker.add_change(change)
193
+
194
+ # Apply to all tenants in the branch
195
+ from cinchdb.managers.change_applier import ChangeApplier
196
+
197
+ applier = ChangeApplier(self.project_root, self.database, self.branch)
198
+ applier.apply_change(change.id)
199
+
200
+ def get_view(self, view_name: str) -> View:
201
+ """Get information about a specific view.
202
+
203
+ Args:
204
+ view_name: Name of the view
205
+
206
+ Returns:
207
+ View object
208
+
209
+ Raises:
210
+ ValueError: If view doesn't exist
211
+ """
212
+ with DatabaseConnection(self.db_path) as conn:
213
+ cursor = conn.execute(
214
+ "SELECT sql FROM sqlite_master WHERE type='view' AND name=?",
215
+ (view_name,),
216
+ )
217
+ row = cursor.fetchone()
218
+
219
+ if not row:
220
+ raise ValueError(f"View '{view_name}' does not exist")
221
+
222
+ # Extract the SQL statement (remove CREATE VIEW ... AS prefix)
223
+ sql = row["sql"]
224
+ # Find the AS keyword and get everything after it
225
+ as_index = sql.upper().find(" AS ")
226
+ if as_index != -1:
227
+ sql_statement = sql[as_index + 4 :].strip()
228
+ else:
229
+ sql_statement = sql
230
+
231
+ return View(
232
+ name=view_name,
233
+ database=self.database,
234
+ branch=self.branch,
235
+ sql_statement=sql_statement,
236
+ )
237
+
238
+ def _view_exists(self, view_name: str) -> bool:
239
+ """Check if a view exists.
240
+
241
+ Args:
242
+ view_name: Name of the view
243
+
244
+ Returns:
245
+ True if view exists
246
+ """
247
+ with DatabaseConnection(self.db_path) as conn:
248
+ cursor = conn.execute(
249
+ "SELECT name FROM sqlite_master WHERE type='view' AND name=?",
250
+ (view_name,),
251
+ )
252
+ return cursor.fetchone() is not None
@@ -0,0 +1,27 @@
1
+ """Core data models for CinchDB."""
2
+
3
+ from .base import CinchDBBaseModel, CinchDBTableModel
4
+ from .project import Project
5
+ from .database import Database
6
+ from .branch import Branch
7
+ from .tenant import Tenant
8
+ from .table import Table, Column, ColumnType, ForeignKeyRef, ForeignKeyAction
9
+ from .view import View
10
+ from .change import Change, ChangeType
11
+
12
+ __all__ = [
13
+ "CinchDBBaseModel",
14
+ "CinchDBTableModel",
15
+ "Project",
16
+ "Database",
17
+ "Branch",
18
+ "Tenant",
19
+ "Table",
20
+ "Column",
21
+ "ColumnType",
22
+ "ForeignKeyRef",
23
+ "ForeignKeyAction",
24
+ "View",
25
+ "Change",
26
+ "ChangeType",
27
+ ]
cinchdb/models/base.py ADDED
@@ -0,0 +1,44 @@
1
+ """Base models for CinchDB."""
2
+
3
+ from datetime import datetime, timezone
4
+ from typing import Optional, Any
5
+ import uuid
6
+ from pydantic import BaseModel, Field, ConfigDict, field_serializer
7
+
8
+
9
+ class CinchDBTableModel(BaseModel):
10
+ """Base model for entities that will be stored as database tables.
11
+
12
+ Includes automatic id, created_at, and updated_at fields.
13
+ """
14
+
15
+ model_config = ConfigDict(
16
+ populate_by_name=True,
17
+ use_enum_values=True,
18
+ )
19
+
20
+ id: str = Field(
21
+ default_factory=lambda: str(uuid.uuid4()), description="Unique identifier"
22
+ )
23
+ created_at: datetime = Field(
24
+ default_factory=lambda: datetime.now(timezone.utc),
25
+ description="Creation timestamp",
26
+ )
27
+ updated_at: Optional[datetime] = Field(
28
+ default=None, description="Last update timestamp"
29
+ )
30
+
31
+ @field_serializer("created_at", "updated_at")
32
+ def serialize_datetime(self, dt: Optional[datetime], _info: Any) -> Optional[str]:
33
+ """Serialize datetime to ISO format."""
34
+ return dt.isoformat() if dt else None
35
+
36
+
37
+ class CinchDBBaseModel(BaseModel):
38
+ """Base model for non-table entities (metadata, config, etc)."""
39
+
40
+ model_config = ConfigDict(
41
+ populate_by_name=True,
42
+ use_enum_values=True,
43
+ extra="forbid", # Strict validation for metadata
44
+ )
@@ -0,0 +1,26 @@
1
+ """Branch model for CinchDB."""
2
+
3
+ from typing import List, Optional, Dict, Any
4
+ from pydantic import Field
5
+ from .base import CinchDBBaseModel
6
+
7
+
8
+ class Branch(CinchDBBaseModel):
9
+ """Represents a branch within a database."""
10
+
11
+ name: str = Field(description="Branch name")
12
+ database: str = Field(description="Parent database name")
13
+ parent_branch: Optional[str] = Field(
14
+ default=None, description="Parent branch this was created from"
15
+ )
16
+ tenants: List[str] = Field(
17
+ default_factory=lambda: ["main"], description="List of tenant names"
18
+ )
19
+ metadata: Dict[str, Any] = Field(
20
+ default_factory=dict, description="Branch metadata"
21
+ )
22
+ is_main: bool = Field(default=False, description="Whether this is the main branch")
23
+
24
+ def can_delete(self) -> bool:
25
+ """Check if this branch can be deleted."""
26
+ return self.name != "main" and not self.is_main
@@ -0,0 +1,47 @@
1
+ """Change tracking models for CinchDB."""
2
+
3
+ from enum import Enum
4
+ from typing import Dict, Any, Optional
5
+ from pydantic import Field
6
+ from .base import CinchDBTableModel
7
+
8
+
9
+ class ChangeType(str, Enum):
10
+ """Types of schema changes."""
11
+
12
+ # Table changes
13
+ CREATE_TABLE = "create_table"
14
+ DROP_TABLE = "drop_table"
15
+ RENAME_TABLE = "rename_table"
16
+
17
+ # Column changes
18
+ ADD_COLUMN = "add_column"
19
+ DROP_COLUMN = "drop_column"
20
+ RENAME_COLUMN = "rename_column"
21
+ MODIFY_COLUMN = "modify_column"
22
+ ALTER_COLUMN_NULLABLE = "alter_column_nullable"
23
+
24
+ # View changes
25
+ CREATE_VIEW = "create_view"
26
+ DROP_VIEW = "drop_view"
27
+ UPDATE_VIEW = "update_view"
28
+
29
+ # Index changes
30
+ CREATE_INDEX = "create_index"
31
+ DROP_INDEX = "drop_index"
32
+
33
+
34
+ class Change(CinchDBTableModel):
35
+ """Represents a schema change in the database."""
36
+
37
+ type: ChangeType = Field(description="Type of change")
38
+ entity_type: str = Field(description="Type of entity affected (table, view, index)")
39
+ entity_name: str = Field(description="Name of the affected entity")
40
+ details: Dict[str, Any] = Field(
41
+ default_factory=dict, description="Change-specific details"
42
+ )
43
+ sql: Optional[str] = Field(
44
+ default=None, description="SQL statement that implements the change"
45
+ )
46
+ branch: str = Field(description="Branch where change was made")
47
+ applied: bool = Field(default=False, description="Whether change has been applied")
@@ -0,0 +1,20 @@
1
+ """Database model for CinchDB."""
2
+
3
+ from typing import List, Optional
4
+ from pydantic import Field
5
+ from .base import CinchDBBaseModel
6
+
7
+
8
+ class Database(CinchDBBaseModel):
9
+ """Represents a database within a CinchDB project."""
10
+
11
+ name: str = Field(description="Database name")
12
+ branches: List[str] = Field(
13
+ default_factory=lambda: ["main"], description="List of branch names"
14
+ )
15
+ active_branch: str = Field(default="main", description="Currently active branch")
16
+ description: Optional[str] = Field(default=None, description="Database description")
17
+
18
+ def can_delete(self) -> bool:
19
+ """Check if this database can be deleted."""
20
+ return self.name != "main"
@@ -0,0 +1,20 @@
1
+ """Project model for CinchDB."""
2
+
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+ from pydantic import Field
6
+ from .base import CinchDBBaseModel
7
+
8
+
9
+ class Project(CinchDBBaseModel):
10
+ """Represents a CinchDB project."""
11
+
12
+ name: str = Field(description="Project name")
13
+ path: Path = Field(description="Path to project directory")
14
+ databases: List[str] = Field(
15
+ default_factory=list, description="List of database names"
16
+ )
17
+ active_database: str = Field(
18
+ default="main", description="Currently active database"
19
+ )
20
+ description: Optional[str] = Field(default=None, description="Project description")
@@ -0,0 +1,86 @@
1
+ """Table and Column models for CinchDB."""
2
+
3
+ from typing import List, Optional, Literal
4
+ from pydantic import BaseModel, Field, ConfigDict
5
+ from .base import CinchDBBaseModel
6
+
7
+
8
+ # SQLite column types
9
+ ColumnType = Literal["TEXT", "INTEGER", "REAL", "BLOB", "NUMERIC"]
10
+
11
+ # Foreign key actions
12
+ ForeignKeyAction = Literal["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]
13
+
14
+
15
+ class ForeignKeyRef(BaseModel):
16
+ """Foreign key reference specification."""
17
+
18
+ model_config = ConfigDict(extra="forbid")
19
+
20
+ table: str = Field(description="Referenced table name")
21
+ column: str = Field(default="id", description="Referenced column name")
22
+ on_delete: ForeignKeyAction = Field(
23
+ default="RESTRICT",
24
+ description="Action on delete of referenced row"
25
+ )
26
+ on_update: ForeignKeyAction = Field(
27
+ default="RESTRICT",
28
+ description="Action on update of referenced row"
29
+ )
30
+
31
+
32
+ class Column(BaseModel):
33
+ """Represents a column in a table."""
34
+
35
+ model_config = ConfigDict(extra="forbid")
36
+
37
+ name: str = Field(description="Column name")
38
+ type: ColumnType = Field(description="SQLite column type")
39
+ nullable: bool = Field(
40
+ default=True, description="Whether column allows NULL values"
41
+ )
42
+ default: Optional[str] = Field(
43
+ default=None, description="Default value SQL expression"
44
+ )
45
+ primary_key: bool = Field(
46
+ default=False, description="Whether this is a primary key"
47
+ )
48
+ unique: bool = Field(default=False, description="Whether values must be unique")
49
+ foreign_key: Optional[ForeignKeyRef] = Field(
50
+ default=None,
51
+ description="Foreign key constraint specification"
52
+ )
53
+
54
+
55
+ class Table(CinchDBBaseModel):
56
+ """Represents a table in the database."""
57
+
58
+ name: str = Field(description="Table name")
59
+ database: str = Field(description="Database name")
60
+ branch: str = Field(description="Branch name")
61
+ columns: List[Column] = Field(default_factory=list, description="Table columns")
62
+
63
+ def __init__(self, **data):
64
+ """Initialize table with default columns."""
65
+ super().__init__(**data)
66
+
67
+ # Add default columns if not present
68
+ column_names = {col.name for col in self.columns}
69
+
70
+ if "id" not in column_names:
71
+ self.columns.insert(
72
+ 0,
73
+ Column(
74
+ name="id",
75
+ type="TEXT",
76
+ nullable=False,
77
+ primary_key=True,
78
+ unique=True,
79
+ ),
80
+ )
81
+
82
+ if "created_at" not in column_names:
83
+ self.columns.append(Column(name="created_at", type="TEXT", nullable=False))
84
+
85
+ if "updated_at" not in column_names:
86
+ self.columns.append(Column(name="updated_at", type="TEXT", nullable=True))
@@ -0,0 +1,19 @@
1
+ """Tenant model for CinchDB."""
2
+
3
+ from typing import Optional
4
+ from pydantic import Field
5
+ from .base import CinchDBBaseModel
6
+
7
+
8
+ class Tenant(CinchDBBaseModel):
9
+ """Represents a tenant within a branch."""
10
+
11
+ name: str = Field(description="Tenant name")
12
+ branch: str = Field(description="Parent branch name")
13
+ database: str = Field(description="Parent database name")
14
+ description: Optional[str] = Field(default=None, description="Tenant description")
15
+ is_main: bool = Field(default=False, description="Whether this is the main tenant")
16
+
17
+ def can_delete(self) -> bool:
18
+ """Check if this tenant can be deleted."""
19
+ return self.name != "main" and not self.is_main
cinchdb/models/view.py ADDED
@@ -0,0 +1,15 @@
1
+ """View model for CinchDB."""
2
+
3
+ from typing import Optional
4
+ from pydantic import Field
5
+ from .base import CinchDBBaseModel
6
+
7
+
8
+ class View(CinchDBBaseModel):
9
+ """Represents a SQL view in the database."""
10
+
11
+ name: str = Field(description="View name")
12
+ database: str = Field(description="Database name")
13
+ branch: str = Field(description="Branch name")
14
+ sql_statement: str = Field(description="SQL SELECT statement that defines the view")
15
+ description: Optional[str] = Field(default=None, description="View description")
@@ -0,0 +1,15 @@
1
+ """Utility modules for CinchDB."""
2
+
3
+ from cinchdb.utils.sql_validator import (
4
+ validate_sql_query,
5
+ validate_query_safe,
6
+ SQLValidationError,
7
+ SQLOperation
8
+ )
9
+
10
+ __all__ = [
11
+ "validate_sql_query",
12
+ "validate_query_safe",
13
+ "SQLValidationError",
14
+ "SQLOperation"
15
+ ]