cinchdb 0.1.8__tar.gz → 0.1.10__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.
Files changed (56) hide show
  1. {cinchdb-0.1.8 → cinchdb-0.1.10}/PKG-INFO +1 -1
  2. {cinchdb-0.1.8 → cinchdb-0.1.10}/pyproject.toml +1 -1
  3. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/path_utils.py +11 -6
  4. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/tenant.py +135 -40
  5. {cinchdb-0.1.8 → cinchdb-0.1.10}/.gitignore +0 -0
  6. {cinchdb-0.1.8 → cinchdb-0.1.10}/LICENSE +0 -0
  7. {cinchdb-0.1.8 → cinchdb-0.1.10}/README.md +0 -0
  8. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/__init__.py +0 -0
  9. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/__main__.py +0 -0
  10. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/__init__.py +0 -0
  11. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/__init__.py +0 -0
  12. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/branch.py +0 -0
  13. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/codegen.py +0 -0
  14. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/column.py +0 -0
  15. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/database.py +0 -0
  16. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/index.py +0 -0
  17. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/query.py +0 -0
  18. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/remote.py +0 -0
  19. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/table.py +0 -0
  20. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/tenant.py +0 -0
  21. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/commands/view.py +0 -0
  22. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/handlers/__init__.py +0 -0
  23. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
  24. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/main.py +0 -0
  25. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/cli/utils.py +0 -0
  26. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/config.py +0 -0
  27. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/__init__.py +0 -0
  28. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/connection.py +0 -0
  29. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/database.py +0 -0
  30. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/initializer.py +0 -0
  31. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/core/maintenance.py +0 -0
  32. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/__init__.py +0 -0
  33. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/branch.py +0 -0
  34. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/change_applier.py +0 -0
  35. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/change_comparator.py +0 -0
  36. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/change_tracker.py +0 -0
  37. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/codegen.py +0 -0
  38. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/column.py +0 -0
  39. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/data.py +0 -0
  40. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/index.py +0 -0
  41. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/merge_manager.py +0 -0
  42. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/query.py +0 -0
  43. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/table.py +0 -0
  44. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/managers/view.py +0 -0
  45. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/__init__.py +0 -0
  46. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/base.py +0 -0
  47. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/branch.py +0 -0
  48. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/change.py +0 -0
  49. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/database.py +0 -0
  50. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/project.py +0 -0
  51. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/table.py +0 -0
  52. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/tenant.py +0 -0
  53. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/models/view.py +0 -0
  54. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/utils/__init__.py +0 -0
  55. {cinchdb-0.1.8 → cinchdb-0.1.10}/src/cinchdb/utils/name_validator.py +0 -0
  56. {cinchdb-0.1.8 → cinchdb-0.1.10}/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.8
3
+ Version: 0.1.10
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinchdb"
3
- version = "0.1.8"
3
+ version = "0.1.10"
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"
@@ -144,10 +144,15 @@ def list_tenants(project_root: Path, database: str, branch: str) -> List[str]:
144
144
  if not tenants_dir.exists():
145
145
  return []
146
146
 
147
- # Only list .db files, not WAL or SHM files
148
- tenants = []
147
+ # List both .db files and .meta files for lazy tenants
148
+ tenants = set()
149
149
  for f in tenants_dir.iterdir():
150
- if f.is_file() and f.suffix == ".db":
151
- tenants.append(f.stem)
152
-
153
- return sorted(tenants)
150
+ if f.is_file():
151
+ if f.suffix == ".db":
152
+ tenants.add(f.stem)
153
+ elif f.suffix == ".meta" and f.name.startswith("."):
154
+ # Lazy tenant metadata files are named .{tenant_name}.meta
155
+ tenant_name = f.stem[1:] # Remove leading dot
156
+ tenants.add(tenant_name)
157
+
158
+ return sorted(list(tenants))
@@ -52,13 +52,14 @@ class TenantManager:
52
52
  return tenants
53
53
 
54
54
  def create_tenant(
55
- self, tenant_name: str, description: Optional[str] = None
55
+ self, tenant_name: str, description: Optional[str] = None, lazy: bool = False
56
56
  ) -> Tenant:
57
57
  """Create a new tenant by copying schema from main tenant.
58
58
 
59
59
  Args:
60
60
  tenant_name: Name for the new tenant
61
61
  description: Optional description
62
+ lazy: If True, don't create database file until first use
62
63
 
63
64
  Returns:
64
65
  Created Tenant object
@@ -74,40 +75,64 @@ class TenantManager:
74
75
  # Check maintenance mode
75
76
  check_maintenance_mode(self.project_root, self.database, self.branch)
76
77
 
77
- # Validate tenant doesn't exist
78
- if tenant_name in list_tenants(self.project_root, self.database, self.branch):
79
- raise ValueError(f"Tenant '{tenant_name}' already exists")
80
-
81
- # Get paths
82
- main_db_path = get_tenant_db_path(
83
- self.project_root, self.database, self.branch, "main"
84
- )
78
+ # Check if tenant metadata already exists
79
+ tenants_dir = self.branch_path / "tenants"
80
+ tenant_meta_file = tenants_dir / f".{tenant_name}.meta"
85
81
  new_db_path = get_tenant_db_path(
86
82
  self.project_root, self.database, self.branch, tenant_name
87
83
  )
88
-
89
- # Copy main tenant database to new tenant
90
- shutil.copy2(main_db_path, new_db_path)
91
-
92
- # Clear any data from the copied database (keep schema only)
93
- with DatabaseConnection(new_db_path) as conn:
94
- # Get all tables
95
- result = conn.execute("""
96
- SELECT name FROM sqlite_master
97
- WHERE type='table'
98
- AND name NOT LIKE 'sqlite_%'
99
- """)
100
- tables = [row["name"] for row in result.fetchall()]
101
-
102
- # Clear data from each table
103
- for table in tables:
104
- conn.execute(f"DELETE FROM {table}")
105
-
106
- conn.commit()
107
84
 
108
- # Open a new connection to vacuum the database
109
- with DatabaseConnection(new_db_path) as conn:
110
- conn.execute("VACUUM")
85
+ # Validate tenant doesn't exist (either as file or metadata)
86
+ if new_db_path.exists() or tenant_meta_file.exists():
87
+ raise ValueError(f"Tenant '{tenant_name}' already exists")
88
+
89
+ if lazy:
90
+ # Just create metadata file, don't create actual database
91
+ tenants_dir.mkdir(parents=True, exist_ok=True)
92
+ import json
93
+ from datetime import datetime, timezone
94
+
95
+ metadata = {
96
+ "name": tenant_name,
97
+ "description": description,
98
+ "created_at": datetime.now(timezone.utc).isoformat(),
99
+ "lazy": True
100
+ }
101
+
102
+ with open(tenant_meta_file, 'w') as f:
103
+ json.dump(metadata, f)
104
+ else:
105
+ # Create actual database file (existing behavior)
106
+ main_db_path = get_tenant_db_path(
107
+ self.project_root, self.database, self.branch, "main"
108
+ )
109
+
110
+ # Copy main tenant database to new tenant
111
+ shutil.copy2(main_db_path, new_db_path)
112
+
113
+ # Clear any data from the copied database (keep schema only)
114
+ with DatabaseConnection(new_db_path) as conn:
115
+ # Get all tables
116
+ result = conn.execute("""
117
+ SELECT name FROM sqlite_master
118
+ WHERE type='table'
119
+ AND name NOT LIKE 'sqlite_%'
120
+ """)
121
+ tables = [row["name"] for row in result.fetchall()]
122
+
123
+ # Clear data from each table
124
+ for table in tables:
125
+ conn.execute(f"DELETE FROM {table}")
126
+
127
+ conn.commit()
128
+
129
+ # Vacuum the database to reduce size
130
+ # Must use raw sqlite3 connection with autocommit mode for VACUUM
131
+ import sqlite3
132
+ vacuum_conn = sqlite3.connect(str(new_db_path))
133
+ vacuum_conn.isolation_level = None # Autocommit mode required for VACUUM
134
+ vacuum_conn.execute("VACUUM")
135
+ vacuum_conn.close()
111
136
 
112
137
  return Tenant(
113
138
  name=tenant_name,
@@ -140,20 +165,90 @@ class TenantManager:
140
165
  ):
141
166
  raise ValueError(f"Tenant '{tenant_name}' does not exist")
142
167
 
143
- # Delete tenant database file and related files
168
+ # Check for and delete metadata file (for lazy tenants)
169
+ tenants_dir = self.branch_path / "tenants"
170
+ meta_file = tenants_dir / f".{tenant_name}.meta"
171
+ if meta_file.exists():
172
+ meta_file.unlink()
173
+
174
+ # Delete tenant database file and related files (if they exist)
144
175
  db_path = get_tenant_db_path(
145
176
  self.project_root, self.database, self.branch, tenant_name
146
177
  )
147
- db_path.unlink()
178
+ if db_path.exists():
179
+ db_path.unlink()
148
180
 
149
- # Also remove WAL and SHM files if they exist
150
- wal_path = db_path.with_suffix(".db-wal")
151
- shm_path = db_path.with_suffix(".db-shm")
181
+ # Also remove WAL and SHM files if they exist
182
+ wal_path = db_path.with_suffix(".db-wal")
183
+ shm_path = db_path.with_suffix(".db-shm")
152
184
 
153
- if wal_path.exists():
154
- wal_path.unlink()
155
- if shm_path.exists():
156
- shm_path.unlink()
185
+ if wal_path.exists():
186
+ wal_path.unlink()
187
+ if shm_path.exists():
188
+ shm_path.unlink()
189
+
190
+ def materialize_tenant(self, tenant_name: str) -> None:
191
+ """Materialize a lazy tenant into an actual database file.
192
+
193
+ Args:
194
+ tenant_name: Name of the tenant to materialize
195
+
196
+ Raises:
197
+ ValueError: If tenant doesn't exist or is already materialized
198
+ """
199
+ tenants_dir = self.branch_path / "tenants"
200
+ tenant_meta_file = tenants_dir / f".{tenant_name}.meta"
201
+ db_path = get_tenant_db_path(
202
+ self.project_root, self.database, self.branch, tenant_name
203
+ )
204
+
205
+ # Check if already materialized
206
+ if db_path.exists():
207
+ return # Already materialized
208
+
209
+ # Check if metadata exists
210
+ if not tenant_meta_file.exists():
211
+ raise ValueError(f"Tenant '{tenant_name}' does not exist")
212
+
213
+ # Get main tenant path for schema copy
214
+ main_db_path = get_tenant_db_path(
215
+ self.project_root, self.database, self.branch, "main"
216
+ )
217
+
218
+ # Copy main tenant database to new tenant
219
+ shutil.copy2(main_db_path, db_path)
220
+
221
+ # Clear any data from the copied database (keep schema only)
222
+ with DatabaseConnection(db_path) as conn:
223
+ # Get all tables
224
+ result = conn.execute("""
225
+ SELECT name FROM sqlite_master
226
+ WHERE type='table'
227
+ AND name NOT LIKE 'sqlite_%'
228
+ """)
229
+ tables = [row["name"] for row in result.fetchall()]
230
+
231
+ # Clear data from each table
232
+ for table in tables:
233
+ conn.execute(f"DELETE FROM {table}")
234
+
235
+ conn.commit()
236
+
237
+ # Vacuum the database to reduce size
238
+ import sqlite3
239
+ vacuum_conn = sqlite3.connect(str(db_path))
240
+ vacuum_conn.isolation_level = None
241
+ vacuum_conn.execute("VACUUM")
242
+ vacuum_conn.close()
243
+
244
+ # Update metadata to indicate it's no longer lazy
245
+ import json
246
+ with open(tenant_meta_file, 'r') as f:
247
+ metadata = json.load(f)
248
+ metadata['lazy'] = False
249
+ metadata['materialized_at'] = Path(db_path).stat().st_mtime
250
+ with open(tenant_meta_file, 'w') as f:
251
+ json.dump(metadata, f)
157
252
 
158
253
  def copy_tenant(self, source_tenant: str, target_tenant: str) -> Tenant:
159
254
  """Copy a tenant to a new tenant.
File without changes
File without changes
File without changes
File without changes