cinchdb 0.1.15__tar.gz → 0.1.18__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 (70) hide show
  1. {cinchdb-0.1.15 → cinchdb-0.1.18}/PKG-INFO +39 -1
  2. {cinchdb-0.1.15 → cinchdb-0.1.18}/README.md +37 -0
  3. {cinchdb-0.1.15 → cinchdb-0.1.18}/pyproject.toml +9 -1
  4. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/tenant.py +50 -2
  5. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/config.py +4 -13
  6. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/core/connection.py +57 -19
  7. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/core/database.py +39 -12
  8. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/core/initializer.py +25 -40
  9. cinchdb-0.1.18/src/cinchdb/core/maintenance_utils.py +43 -0
  10. cinchdb-0.1.18/src/cinchdb/core/path_utils.py +348 -0
  11. cinchdb-0.1.18/src/cinchdb/core/tenant_activation.py +216 -0
  12. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/infrastructure/metadata_db.py +96 -3
  13. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/change_applier.py +22 -23
  14. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/codegen.py +372 -15
  15. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/column.py +8 -5
  16. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/data.py +17 -14
  17. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/query.py +8 -5
  18. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/table.py +9 -6
  19. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/tenant.py +228 -72
  20. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/view.py +1 -1
  21. cinchdb-0.1.18/src/cinchdb/plugins/__init__.py +16 -0
  22. cinchdb-0.1.18/src/cinchdb/plugins/base.py +80 -0
  23. cinchdb-0.1.18/src/cinchdb/plugins/decorators.py +49 -0
  24. cinchdb-0.1.18/src/cinchdb/plugins/manager.py +210 -0
  25. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/utils/name_validator.py +22 -12
  26. cinchdb-0.1.15/src/cinchdb/core/maintenance.py +0 -73
  27. cinchdb-0.1.15/src/cinchdb/core/path_utils.py +0 -175
  28. cinchdb-0.1.15/src/cinchdb/plugins/__init__.py +0 -17
  29. cinchdb-0.1.15/src/cinchdb/plugins/base.py +0 -99
  30. cinchdb-0.1.15/src/cinchdb/plugins/decorators.py +0 -45
  31. cinchdb-0.1.15/src/cinchdb/plugins/manager.py +0 -178
  32. {cinchdb-0.1.15 → cinchdb-0.1.18}/.gitignore +0 -0
  33. {cinchdb-0.1.15 → cinchdb-0.1.18}/LICENSE +0 -0
  34. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/__init__.py +0 -0
  35. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/__main__.py +0 -0
  36. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/__init__.py +0 -0
  37. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/__init__.py +0 -0
  38. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/branch.py +0 -0
  39. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/codegen.py +0 -0
  40. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/column.py +0 -0
  41. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/data.py +0 -0
  42. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/database.py +0 -0
  43. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/index.py +0 -0
  44. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/query.py +0 -0
  45. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/remote.py +0 -0
  46. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/table.py +0 -0
  47. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/commands/view.py +0 -0
  48. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/handlers/__init__.py +0 -0
  49. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
  50. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/main.py +0 -0
  51. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/cli/utils.py +0 -0
  52. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/core/__init__.py +0 -0
  53. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/infrastructure/metadata_connection_pool.py +0 -0
  54. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/__init__.py +0 -0
  55. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/branch.py +0 -0
  56. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/change_comparator.py +0 -0
  57. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/change_tracker.py +0 -0
  58. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/index.py +0 -0
  59. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/managers/merge_manager.py +0 -0
  60. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/__init__.py +0 -0
  61. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/base.py +0 -0
  62. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/branch.py +0 -0
  63. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/change.py +0 -0
  64. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/database.py +0 -0
  65. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/project.py +0 -0
  66. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/table.py +0 -0
  67. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/tenant.py +0 -0
  68. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/models/view.py +0 -0
  69. {cinchdb-0.1.15 → cinchdb-0.1.18}/src/cinchdb/utils/__init__.py +0 -0
  70. {cinchdb-0.1.15 → cinchdb-0.1.18}/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.15
3
+ Version: 0.1.18
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,6 +18,7 @@ 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: httpx>=0.25.0
21
22
  Requires-Dist: pydantic>=2.0.0
22
23
  Requires-Dist: requests>=2.28.0
23
24
  Requires-Dist: rich>=13.0.0
@@ -31,6 +32,10 @@ Description-Content-Type: text/markdown
31
32
 
32
33
  **Git-like SQLite database management with branching and multi-tenancy**
33
34
 
35
+ [![PyPI version](https://badge.fury.io/py/cinchdb.svg)](https://badge.fury.io/py/cinchdb)
36
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
37
+
38
+
34
39
  NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
35
40
 
36
41
  CinchDB is for projects that need fast queries, 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.
@@ -64,6 +69,10 @@ cinch branch merge-into-main feature
64
69
  cinch tenant create customer_a
65
70
  cinch query "SELECT * FROM users" --tenant customer_a
66
71
 
72
+ # Tenant encryption (bring your own keys)
73
+ cinch tenant create secure_customer --encrypt --key="your-secret-key"
74
+ cinch query "SELECT * FROM users" --tenant secure_customer --key="your-secret-key"
75
+
67
76
  # Future: Remote connectivity planned for production deployment
68
77
 
69
78
  # Autogenerate Python SDK from database
@@ -153,6 +162,35 @@ db.update("posts", post_id, {"content": "Updated content"})
153
162
 
154
163
  ## Architecture
155
164
 
165
+ ### Storage Architecture
166
+
167
+ CinchDB uses a **tenant-first storage model** where database and branch are organizational metadata concepts, while tenants represent the actual isolated data stores:
168
+
169
+ ```
170
+ .cinchdb/
171
+ ├── metadata.db # Organizational metadata
172
+ └── {database}-{branch}/ # Context root (e.g., main-main, prod-feature)
173
+ ├── {shard}/ # SHA256-based sharding (first 2 chars)
174
+ │ ├── {tenant}.db # Actual SQLite database
175
+ │ └── {tenant}.db-wal # WAL file
176
+ └── ...
177
+ ```
178
+
179
+ **Key Design Decisions:**
180
+ - **Tenant-first**: Each tenant gets its own SQLite database file
181
+ - **Flat hierarchy**: Database/branch form a single context root, avoiding deep nesting
182
+ - **Hash sharding**: Tenants are distributed across 256 shards using SHA256 for scalability
183
+ - **Lazy initialization**: Tenant databases are created on first access, not on tenant creation
184
+ - **WAL mode**: All databases use Write-Ahead Logging for better concurrency
185
+
186
+ This architecture enables:
187
+ - True multi-tenant isolation at the file system level
188
+ - Efficient branching without duplicating tenant data
189
+ - Simple backup/restore per tenant
190
+ - Horizontal scaling through sharding
191
+
192
+ ### Components
193
+
156
194
  - **Python SDK**: Core functionality for local development
157
195
  - **CLI**: Full-featured command-line interface
158
196
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  **Git-like SQLite database management with branching and multi-tenancy**
4
4
 
5
+ [![PyPI version](https://badge.fury.io/py/cinchdb.svg)](https://badge.fury.io/py/cinchdb)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
7
+
8
+
5
9
  NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
6
10
 
7
11
  CinchDB is for projects that need fast queries, 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,6 +39,10 @@ cinch branch merge-into-main feature
35
39
  cinch tenant create customer_a
36
40
  cinch query "SELECT * FROM users" --tenant customer_a
37
41
 
42
+ # Tenant encryption (bring your own keys)
43
+ cinch tenant create secure_customer --encrypt --key="your-secret-key"
44
+ cinch query "SELECT * FROM users" --tenant secure_customer --key="your-secret-key"
45
+
38
46
  # Future: Remote connectivity planned for production deployment
39
47
 
40
48
  # Autogenerate Python SDK from database
@@ -124,6 +132,35 @@ db.update("posts", post_id, {"content": "Updated content"})
124
132
 
125
133
  ## Architecture
126
134
 
135
+ ### Storage Architecture
136
+
137
+ CinchDB uses a **tenant-first storage model** where database and branch are organizational metadata concepts, while tenants represent the actual isolated data stores:
138
+
139
+ ```
140
+ .cinchdb/
141
+ ├── metadata.db # Organizational metadata
142
+ └── {database}-{branch}/ # Context root (e.g., main-main, prod-feature)
143
+ ├── {shard}/ # SHA256-based sharding (first 2 chars)
144
+ │ ├── {tenant}.db # Actual SQLite database
145
+ │ └── {tenant}.db-wal # WAL file
146
+ └── ...
147
+ ```
148
+
149
+ **Key Design Decisions:**
150
+ - **Tenant-first**: Each tenant gets its own SQLite database file
151
+ - **Flat hierarchy**: Database/branch form a single context root, avoiding deep nesting
152
+ - **Hash sharding**: Tenants are distributed across 256 shards using SHA256 for scalability
153
+ - **Lazy initialization**: Tenant databases are created on first access, not on tenant creation
154
+ - **WAL mode**: All databases use Write-Ahead Logging for better concurrency
155
+
156
+ This architecture enables:
157
+ - True multi-tenant isolation at the file system level
158
+ - Efficient branching without duplicating tenant data
159
+ - Simple backup/restore per tenant
160
+ - Horizontal scaling through sharding
161
+
162
+ ### Components
163
+
127
164
  - **Python SDK**: Core functionality for local development
128
165
  - **CLI**: Full-featured command-line interface
129
166
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cinchdb"
3
- version = "0.1.15"
3
+ version = "0.1.18"
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"
@@ -24,6 +24,7 @@ dependencies = [
24
24
  "typer>=0.9.0",
25
25
  "rich>=13.0.0",
26
26
  "requests>=2.28.0",
27
+ "httpx>=0.25.0",
27
28
  ]
28
29
 
29
30
  [project.optional-dependencies]
@@ -80,7 +81,14 @@ packages = ["src/cinchdb"]
80
81
  dev-dependencies = [
81
82
  "pytest>=8.0.0",
82
83
  "pytest-cov>=4.0.0",
84
+ "pytest-asyncio>=0.21.0",
85
+ "pytest-timeout>=2.4.0",
83
86
  "ruff>=0.5.0",
84
87
  "mypy>=1.0.0",
85
88
  "responses>=0.23.0",
86
89
  ]
90
+
91
+ [tool.pytest.ini_options]
92
+ timeout = 5
93
+ timeout_method = "thread"
94
+ addopts = "--timeout=5 --timeout-method=thread"
@@ -68,10 +68,25 @@ def create(
68
68
  description: Optional[str] = typer.Option(
69
69
  None, "--description", "-d", help="Tenant description"
70
70
  ),
71
+ encrypt: bool = typer.Option(
72
+ False, "--encrypt", help="Create encrypted tenant database"
73
+ ),
74
+ key: Optional[str] = typer.Option(
75
+ None, "--key", help="Encryption key for encrypted tenant (required with --encrypt)"
76
+ ),
71
77
  ):
72
78
  """Create a new tenant."""
73
79
  name = validate_required_arg(name, "name", ctx)
74
80
 
81
+ # Validate encryption parameters
82
+ if encrypt and not key:
83
+ console.print("[red]❌ --key is required when using --encrypt[/red]")
84
+ raise typer.Exit(1)
85
+
86
+ if key and not encrypt:
87
+ console.print("[red]❌ --encrypt is required when using --key[/red]")
88
+ raise typer.Exit(1)
89
+
75
90
  # Validate tenant name
76
91
  try:
77
92
  validate_name(name, "tenant")
@@ -85,8 +100,13 @@ def create(
85
100
 
86
101
  try:
87
102
  tenant_mgr = TenantManager(config.project_dir, db_name, branch_name)
88
- tenant_mgr.create_tenant(name, description)
89
- console.print(f"[green]✅ Created tenant '{name}'[/green]")
103
+ tenant_mgr.create_tenant(name, description, encrypt=encrypt, encryption_key=key)
104
+
105
+ if encrypt:
106
+ console.print(f"[green]✅ Created encrypted tenant '{name}'[/green]")
107
+ console.print("[yellow]Note: Keep your encryption key secure - losing it means losing your data[/yellow]")
108
+ else:
109
+ console.print(f"[green]✅ Created tenant '{name}'[/green]")
90
110
  console.print("[yellow]Note: Tenant has same schema as main tenant[/yellow]")
91
111
 
92
112
  except ValueError as e:
@@ -235,3 +255,31 @@ def vacuum(
235
255
  except ValueError as e:
236
256
  console.print(f"[red]❌ {e}[/red]")
237
257
  raise typer.Exit(1)
258
+
259
+
260
+ @app.command(name="rotate-key")
261
+ def rotate_key(
262
+ tenant_name: str = typer.Argument(..., help="Name of the tenant to rotate encryption key for"),
263
+ ):
264
+ """Rotate encryption key for a tenant (requires plugged extension)."""
265
+ validate_required_arg(tenant_name, "tenant name")
266
+
267
+ config, config_data = get_config_with_data()
268
+ db_name = config_data.active_database
269
+ branch_name = config_data.active_branch
270
+
271
+ try:
272
+ tenant_mgr = TenantManager(config.project_dir, db_name, branch_name)
273
+
274
+ console.print(f"[yellow]🔐 Rotating encryption key for tenant '{tenant_name}'...[/yellow]")
275
+
276
+ new_key = tenant_mgr.rotate_tenant_key(tenant_name)
277
+
278
+ console.print("[green]✅ Encryption key rotated successfully[/green]")
279
+ console.print(f" Tenant: {tenant_name}")
280
+ console.print(f" New key generated (version incremented)")
281
+ console.print("[blue]ℹ️ Historical data remains accessible with previous key versions[/blue]")
282
+
283
+ except ValueError as e:
284
+ console.print(f"[red]❌ {e}[/red]")
285
+ raise typer.Exit(1)
@@ -134,17 +134,8 @@ class Config:
134
134
  with open(self.config_path, "w") as f:
135
135
  toml.dump(config_dict, f)
136
136
 
137
- def init_project(self) -> ProjectConfig:
138
- """Initialize a new CinchDB project with default configuration.
139
-
140
- This method now delegates to the ProjectInitializer for the actual
141
- initialization logic.
142
- """
143
- from cinchdb.core.initializer import ProjectInitializer
144
-
145
- initializer = ProjectInitializer(self.project_dir)
146
- config = initializer.init_project()
137
+ @property
138
+ def base_dir(self) -> Path:
139
+ """Get the base project directory."""
140
+ return self.project_dir
147
141
 
148
- # Load the config into this instance
149
- self._config = config
150
- return config
@@ -1,8 +1,9 @@
1
1
  """SQLite connection management for CinchDB."""
2
2
 
3
+ import os
3
4
  import sqlite3
4
5
  from pathlib import Path
5
- from typing import Optional, Dict, List
6
+ from typing import Optional, Dict, List, Any
6
7
  from contextlib import contextmanager
7
8
  from datetime import datetime
8
9
 
@@ -28,13 +29,19 @@ sqlite3.register_converter("DATETIME", convert_datetime)
28
29
  class DatabaseConnection:
29
30
  """Manages a SQLite database connection with WAL mode."""
30
31
 
31
- def __init__(self, path: Path):
32
+ def __init__(self, path: Path, tenant_id: Optional[str] = None, encryption_manager=None, encryption_key: Optional[str] = None):
32
33
  """Initialize database connection.
33
34
 
34
35
  Args:
35
36
  path: Path to SQLite database file
37
+ tenant_id: Tenant ID for per-tenant encryption
38
+ encryption_manager: EncryptionManager instance for encrypted connections
39
+ encryption_key: Encryption key for encrypted databases
36
40
  """
37
41
  self.path = Path(path)
42
+ self.tenant_id = tenant_id
43
+ self.encryption_manager = encryption_manager
44
+ self.encryption_key = encryption_key
38
45
  self._conn: Optional[sqlite3.Connection] = None
39
46
  self._connect()
40
47
 
@@ -43,19 +50,45 @@ class DatabaseConnection:
43
50
  # Ensure directory exists
44
51
  self.path.parent.mkdir(parents=True, exist_ok=True)
45
52
 
46
- # Connect with row factory for dict-like access
47
- # detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
48
- self._conn = sqlite3.connect(
49
- str(self.path),
50
- detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
51
- )
53
+ # Use EncryptionManager if available
54
+ if self.encryption_manager:
55
+ self._conn = self.encryption_manager.get_connection(self.path, tenant_id=self.tenant_id)
56
+ elif self.encryption_key:
57
+ # Direct encryption key provided - use SQLCipher
58
+ self._conn = sqlite3.connect(
59
+ str(self.path),
60
+ detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
61
+ )
62
+
63
+ # Try to set encryption key (this will fail if SQLCipher is not available)
64
+ try:
65
+ self._conn.execute(f"PRAGMA key = '{self.encryption_key}'")
66
+ except sqlite3.OperationalError as e:
67
+ self._conn.close()
68
+ raise ValueError(
69
+ "SQLCipher is required for encryption but not available. "
70
+ "Please install pysqlcipher3 or sqlite3 with SQLCipher support."
71
+ ) from e
72
+
73
+ # Configure WAL mode and settings
74
+ self._conn.execute("PRAGMA journal_mode = WAL")
75
+ self._conn.execute("PRAGMA synchronous = NORMAL")
76
+ self._conn.execute("PRAGMA wal_autocheckpoint = 0")
77
+ else:
78
+ # Fallback to standard SQLite connection
79
+ # Connect with row factory for dict-like access
80
+ # detect_types=PARSE_DECLTYPES tells SQLite to use our registered converters
81
+ self._conn = sqlite3.connect(
82
+ str(self.path),
83
+ detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES,
84
+ )
85
+
86
+ # Configure WAL mode and settings
87
+ self._conn.execute("PRAGMA journal_mode = WAL")
88
+ self._conn.execute("PRAGMA synchronous = NORMAL")
89
+ self._conn.execute("PRAGMA wal_autocheckpoint = 0")
52
90
 
53
- # Configure WAL mode and settings
54
- self._conn.execute("PRAGMA journal_mode = WAL")
55
- self._conn.execute("PRAGMA synchronous = NORMAL")
56
- self._conn.execute("PRAGMA wal_autocheckpoint = 0")
57
-
58
- # Set row factory and foreign keys
91
+ # Set row factory and foreign keys (both encrypted and unencrypted)
59
92
  self._conn.row_factory = sqlite3.Row
60
93
  self._conn.execute("PRAGMA foreign_keys = ON")
61
94
  self._conn.commit()
@@ -141,23 +174,28 @@ class ConnectionPool:
141
174
 
142
175
  def __init__(self):
143
176
  """Initialize connection pool."""
144
- self._connections: Dict[Path, DatabaseConnection] = {}
177
+ self._connections: Dict[Any, DatabaseConnection] = {}
145
178
 
146
- def get_connection(self, path: Path) -> DatabaseConnection:
179
+ def get_connection(self, path: Path, tenant_id: Optional[str] = None, encryption_manager=None) -> DatabaseConnection:
147
180
  """Get or create a connection for the given path.
148
181
 
149
182
  Args:
150
183
  path: Database file path
184
+ tenant_id: Tenant ID for per-tenant encryption
185
+ encryption_manager: EncryptionManager instance for encrypted connections
151
186
 
152
187
  Returns:
153
188
  Database connection
154
189
  """
155
190
  path = Path(path).resolve()
191
+
192
+ # Create a cache key that includes tenant_id to handle per-tenant connections
193
+ cache_key = (str(path), tenant_id) if tenant_id else str(path)
156
194
 
157
- if path not in self._connections:
158
- self._connections[path] = DatabaseConnection(path)
195
+ if cache_key not in self._connections:
196
+ self._connections[cache_key] = DatabaseConnection(path, tenant_id=tenant_id, encryption_manager=encryption_manager)
159
197
 
160
- return self._connections[path]
198
+ return self._connections[cache_key]
161
199
 
162
200
  def close_connection(self, path: Path) -> None:
163
201
  """Close and remove a specific connection.
@@ -1,11 +1,13 @@
1
1
  """Unified database connection interface for CinchDB."""
2
2
 
3
+ import os
3
4
  from pathlib import Path
4
5
  from typing import List, Dict, Any, Optional, TYPE_CHECKING
5
6
 
6
7
  from cinchdb.models import Column, Change
7
8
  from cinchdb.core.path_utils import get_project_root
8
9
  from cinchdb.utils import validate_query_safe
10
+ from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
9
11
 
10
12
  if TYPE_CHECKING:
11
13
  from cinchdb.managers.table import TableManager
@@ -61,6 +63,8 @@ class CinchDB:
61
63
  project_dir: Optional[Path] = None,
62
64
  api_url: Optional[str] = None,
63
65
  api_key: Optional[str] = None,
66
+ encryption_manager=None,
67
+ encryption_key: Optional[str] = None,
64
68
  ):
65
69
  """Initialize CinchDB connection.
66
70
 
@@ -71,6 +75,8 @@ class CinchDB:
71
75
  project_dir: Path to project directory for local connection
72
76
  api_url: Base URL for remote API connection
73
77
  api_key: API key for remote connection
78
+ encryption_manager: EncryptionManager instance for encrypted connections
79
+ encryption_key: Encryption key for encrypted tenant databases
74
80
 
75
81
  Raises:
76
82
  ValueError: If neither local nor remote connection params provided
@@ -78,7 +84,9 @@ class CinchDB:
78
84
  self.database = database
79
85
  self.branch = branch
80
86
  self.tenant = tenant
81
-
87
+ self.encryption_manager = encryption_manager
88
+ self.encryption_key = encryption_key
89
+
82
90
  # Determine connection type
83
91
  if project_dir is not None:
84
92
  # Local connection
@@ -120,16 +128,29 @@ class CinchDB:
120
128
  return
121
129
 
122
130
  # Check if this is a lazy database using metadata DB
123
- from cinchdb.infrastructure.metadata_db import MetadataDB
124
-
125
- with MetadataDB(self.project_dir) as metadata_db:
126
- db_info = metadata_db.get_database(self.database)
131
+ metadata_db = get_metadata_db(self.project_dir)
132
+ db_info = metadata_db.get_database(self.database)
127
133
 
128
134
  if db_info and not db_info['materialized']:
129
135
  # Database exists in metadata but not materialized
130
136
  from cinchdb.core.initializer import ProjectInitializer
131
137
  initializer = ProjectInitializer(self.project_dir)
132
138
  initializer.materialize_database(self.database)
139
+
140
+ def get_connection(self, db_path, tenant_id: Optional[str] = None, encryption_manager=None, encryption_key: Optional[str] = None) -> "DatabaseConnection":
141
+ """Get a database connection.
142
+
143
+ Args:
144
+ db_path: Path to database file
145
+ tenant_id: Tenant ID for per-tenant encryption
146
+ encryption_manager: EncryptionManager instance for encrypted connections
147
+ encryption_key: Encryption key for encrypted databases
148
+
149
+ Returns:
150
+ DatabaseConnection instance
151
+ """
152
+ from cinchdb.core.connection import DatabaseConnection
153
+ return DatabaseConnection(db_path, tenant_id=tenant_id, encryption_manager=encryption_manager, encryption_key=encryption_key)
133
154
 
134
155
  @property
135
156
  def session(self):
@@ -220,7 +241,7 @@ class CinchDB:
220
241
  from cinchdb.managers.table import TableManager
221
242
 
222
243
  self._table_manager = TableManager(
223
- self.project_dir, self.database, self.branch, self.tenant
244
+ self.project_dir, self.database, self.branch, self.tenant, self.encryption_manager
224
245
  )
225
246
  return self._table_manager
226
247
 
@@ -235,7 +256,7 @@ class CinchDB:
235
256
  from cinchdb.managers.column import ColumnManager
236
257
 
237
258
  self._column_manager = ColumnManager(
238
- self.project_dir, self.database, self.branch, self.tenant
259
+ self.project_dir, self.database, self.branch, self.tenant, self.encryption_manager
239
260
  )
240
261
  return self._column_manager
241
262
 
@@ -278,7 +299,7 @@ class CinchDB:
278
299
  from cinchdb.managers.tenant import TenantManager
279
300
 
280
301
  self._tenant_manager = TenantManager(
281
- self.project_dir, self.database, self.branch
302
+ self.project_dir, self.database, self.branch, self.encryption_manager
282
303
  )
283
304
  return self._tenant_manager
284
305
 
@@ -293,7 +314,7 @@ class CinchDB:
293
314
  from cinchdb.managers.data import DataManager
294
315
 
295
316
  self._data_manager = DataManager(
296
- self.project_dir, self.database, self.branch, self.tenant
317
+ self.project_dir, self.database, self.branch, self.tenant, self.encryption_manager
297
318
  )
298
319
  return self._data_manager
299
320
 
@@ -370,9 +391,9 @@ class CinchDB:
370
391
  from cinchdb.managers.query import QueryManager
371
392
 
372
393
  self._query_manager = QueryManager(
373
- self.project_dir, self.database, self.branch, self.tenant
394
+ self.project_dir, self.database, self.branch, self.tenant, self.encryption_manager
374
395
  )
375
- return self._query_manager.execute(sql, params)
396
+ return self._query_manager.execute(sql, params, skip_validation)
376
397
  else:
377
398
  # Remote query
378
399
  data = {"sql": sql}
@@ -854,6 +875,7 @@ def connect(
854
875
  branch: str = "main",
855
876
  tenant: str = "main",
856
877
  project_dir: Optional[Path] = None,
878
+ encryption_key: Optional[str] = None,
857
879
  ) -> CinchDB:
858
880
  """Connect to a local CinchDB database.
859
881
 
@@ -862,6 +884,7 @@ def connect(
862
884
  branch: Branch name (default: main)
863
885
  tenant: Tenant name (default: main)
864
886
  project_dir: Path to project directory (optional, will search for .cinchdb)
887
+ encryption_key: Encryption key for encrypted tenant databases
865
888
 
866
889
  Returns:
867
890
  CinchDB connection instance
@@ -873,6 +896,9 @@ def connect(
873
896
  # Connect to specific branch
874
897
  db = connect("mydb", "feature-branch")
875
898
 
899
+ # Connect to encrypted tenant
900
+ db = connect("mydb", tenant="customer_a", encryption_key="my-secret-key")
901
+
876
902
  # Connect with explicit project directory
877
903
  db = connect("mydb", project_dir=Path("/path/to/project"))
878
904
  """
@@ -883,7 +909,8 @@ def connect(
883
909
  raise ValueError("No .cinchdb directory found. Run 'cinchdb init' first.")
884
910
 
885
911
  return CinchDB(
886
- database=database, branch=branch, tenant=tenant, project_dir=project_dir
912
+ database=database, branch=branch, tenant=tenant, project_dir=project_dir,
913
+ encryption_key=encryption_key
887
914
  )
888
915
 
889
916
 
@@ -1,6 +1,5 @@
1
1
  """Project initialization for CinchDB."""
2
2
 
3
- import hashlib
4
3
  import json
5
4
  import uuid
6
5
  from datetime import datetime, timezone
@@ -11,19 +10,12 @@ from cinchdb.core.connection import DatabaseConnection
11
10
  from cinchdb.config import ProjectConfig
12
11
  from cinchdb.infrastructure.metadata_db import MetadataDB
13
12
  from cinchdb.infrastructure.metadata_connection_pool import get_metadata_db
14
-
15
-
16
- def _calculate_shard(tenant_name: str) -> str:
17
- """Calculate the shard directory for a tenant using SHA256 hash.
18
-
19
- Args:
20
- tenant_name: Name of the tenant
21
-
22
- Returns:
23
- Two-character hex string (e.g., "a0", "ff")
24
- """
25
- hash_val = hashlib.sha256(tenant_name.encode('utf-8')).hexdigest()
26
- return hash_val[:2]
13
+ from cinchdb.core.path_utils import (
14
+ calculate_shard,
15
+ ensure_tenant_db_path,
16
+ get_context_root,
17
+ ensure_context_directory,
18
+ )
27
19
 
28
20
 
29
21
  class ProjectInitializer:
@@ -106,7 +98,7 @@ class ProjectInitializer:
106
98
 
107
99
  # Also create main tenant in metadata
108
100
  tenant_id = str(uuid.uuid4())
109
- main_shard = _calculate_shard("main")
101
+ main_shard = calculate_shard("main")
110
102
  self.metadata_db.create_tenant(
111
103
  tenant_id, branch_id, "main", main_shard,
112
104
  metadata={"created_at": datetime.now(timezone.utc).isoformat()}
@@ -115,7 +107,7 @@ class ProjectInitializer:
115
107
 
116
108
  # Create __empty__ tenant in metadata (for lazy tenant reads)
117
109
  empty_tenant_id = str(uuid.uuid4())
118
- empty_shard = _calculate_shard("__empty__")
110
+ empty_shard = calculate_shard("__empty__")
119
111
  self.metadata_db.create_tenant(
120
112
  empty_tenant_id, branch_id, "__empty__", empty_shard,
121
113
  metadata={
@@ -188,7 +180,7 @@ class ProjectInitializer:
188
180
 
189
181
  # Create main tenant entry in metadata (will be materialized if database is not lazy)
190
182
  main_tenant_id = str(uuid.uuid4())
191
- main_shard = _calculate_shard("main")
183
+ main_shard = calculate_shard("main")
192
184
  self.metadata_db.create_tenant(
193
185
  main_tenant_id, branch_id, "main", main_shard,
194
186
  metadata={"description": "Default tenant", "created_at": datetime.now(timezone.utc).isoformat()}
@@ -197,7 +189,7 @@ class ProjectInitializer:
197
189
  # Create __empty__ tenant entry in metadata (lazy)
198
190
  # This serves as a template for all lazy tenants in this branch
199
191
  empty_tenant_id = str(uuid.uuid4())
200
- empty_shard = _calculate_shard("__empty__")
192
+ empty_shard = calculate_shard("__empty__")
201
193
  self.metadata_db.create_tenant(
202
194
  empty_tenant_id, branch_id, "__empty__", empty_shard,
203
195
  metadata={"system": True, "description": "Template for lazy tenants"}
@@ -219,44 +211,37 @@ class ProjectInitializer:
219
211
  description: Optional[str] = None,
220
212
  create_tenant_files: bool = False,
221
213
  ) -> None:
222
- """Create the directory structure for a database.
214
+ """Create the directory structure for a database using tenant-first storage.
223
215
 
224
216
  Args:
225
217
  database_name: Name of the database
226
218
  branch_name: Name of the initial branch
227
219
  description: Optional description
220
+ create_tenant_files: Whether to create actual tenant files
228
221
  """
229
- # Create database branch path
230
- branch_path = (
231
- self.config_dir / "databases" / database_name / "branches" / branch_name
232
- )
233
- branch_path.mkdir(parents=True, exist_ok=True)
234
-
235
- # Create metadata file
222
+ # Use tenant-first structure: .cinchdb/{database}-{branch}/
223
+ context_root = ensure_context_directory(self.project_dir, database_name, branch_name)
224
+
225
+ # Create metadata file in context root
236
226
  metadata = {
237
227
  "created_at": datetime.now(timezone.utc).isoformat(),
238
- "name": branch_name,
228
+ "database": database_name,
229
+ "branch": branch_name,
239
230
  "parent": None,
240
231
  "description": description,
241
232
  }
242
-
243
- with open(branch_path / "metadata.json", "w") as f:
233
+
234
+ with open(context_root / "metadata.json", "w") as f:
244
235
  json.dump(metadata, f, indent=2)
245
-
236
+
246
237
  # Create empty changes file
247
- with open(branch_path / "changes.json", "w") as f:
238
+ with open(context_root / "changes.json", "w") as f:
248
239
  json.dump([], f, indent=2)
249
-
250
- # Create tenants directory
251
- tenant_dir = branch_path / "tenants"
252
- tenant_dir.mkdir(exist_ok=True)
253
-
240
+
254
241
  # Create main tenant database in sharded directory (only if requested)
255
242
  if create_tenant_files:
256
- main_shard = _calculate_shard("main")
257
- main_shard_dir = tenant_dir / main_shard
258
- main_shard_dir.mkdir(parents=True, exist_ok=True)
259
- self._init_tenant_database(main_shard_dir / "main.db")
243
+ main_db_path = ensure_tenant_db_path(self.project_dir, database_name, branch_name, "main")
244
+ self._init_tenant_database(main_db_path)
260
245
 
261
246
  def _init_tenant_database(self, db_path: Path) -> None:
262
247
  """Initialize a tenant database with proper PRAGMAs.