cinchdb 0.1.15__py3-none-any.whl → 0.1.18__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/plugins/base.py CHANGED
@@ -1,90 +1,73 @@
1
1
  """
2
- Base classes for CinchDB plugins.
2
+ Simple base class for CinchDB plugins.
3
3
  """
4
4
 
5
- from abc import ABC, abstractmethod
6
- from typing import Any, Dict, List, Callable
7
- from enum import Enum
5
+ from typing import Any, Dict, Optional
8
6
 
9
7
 
10
- class PluginHook(Enum):
11
- """Available plugin hooks."""
12
- # Database lifecycle hooks
13
- DATABASE_INIT = "database_init"
14
- DATABASE_CONNECT = "database_connect"
15
- DATABASE_DISCONNECT = "database_disconnect"
8
+ class Plugin:
9
+ """Simple base class for CinchDB plugins."""
16
10
 
17
- # Query hooks
18
- QUERY_BEFORE = "query_before"
19
- QUERY_AFTER = "query_after"
20
- QUERY_ERROR = "query_error"
11
+ # Plugin metadata - override in subclass
12
+ name: str = "unnamed_plugin"
13
+ version: str = "1.0.0"
14
+ description: str = ""
21
15
 
22
- # Table hooks
23
- TABLE_CREATE = "table_create"
24
- TABLE_DROP = "table_drop"
25
- TABLE_ALTER = "table_alter"
26
-
27
- # Tenant hooks
28
- TENANT_CREATE = "tenant_create"
29
- TENANT_DROP = "tenant_drop"
30
-
31
- # Branch hooks
32
- BRANCH_CREATE = "branch_create"
33
- BRANCH_SWITCH = "branch_switch"
34
- BRANCH_MERGE = "branch_merge"
35
-
36
- # CLI hooks
37
- CLI_COMMAND_BEFORE = "cli_command_before"
38
- CLI_COMMAND_AFTER = "cli_command_after"
39
-
40
-
41
- class BasePlugin(ABC):
42
- """Base class for all CinchDB plugins."""
43
-
44
- def __init__(self):
45
- self.name = self.__class__.__name__
46
- self.version = "1.0.0"
47
- self.description = ""
48
- self._hooks: Dict[PluginHook, List[Callable]] = {}
49
- self._methods: Dict[str, Callable] = {}
16
+ def extend_database(self, db) -> None:
17
+ """Add methods or modify the database instance.
18
+
19
+ Override this method to add custom methods to database instances:
50
20
 
51
- @abstractmethod
52
- def initialize(self, cinchdb_instance) -> None:
53
- """Initialize the plugin with a CinchDB instance."""
21
+ Example:
22
+ def extend_database(self, db):
23
+ db.my_custom_method = self.my_custom_method
24
+ """
54
25
  pass
55
26
 
56
- def register_hook(self, hook: PluginHook, callback: Callable) -> None:
57
- """Register a callback for a specific hook."""
58
- if hook not in self._hooks:
59
- self._hooks[hook] = []
60
- self._hooks[hook].append(callback)
61
-
62
- def register_method(self, method_name: str, method: Callable) -> None:
63
- """Register a new method to be added to CinchDB instances."""
64
- self._methods[method_name] = method
27
+ def before_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
28
+ """Called before executing any SQL query.
29
+
30
+ Args:
31
+ sql: The SQL statement to be executed
32
+ params: Parameters for the SQL statement
33
+
34
+ Returns:
35
+ Tuple of (modified_sql, modified_params)
36
+ """
37
+ return sql, params
65
38
 
66
- def get_hooks(self) -> Dict[PluginHook, List[Callable]]:
67
- """Get all registered hooks."""
68
- return self._hooks.copy()
39
+ def after_query(self, sql: str, params: Optional[tuple], result: Any) -> Any:
40
+ """Called after executing any SQL query.
41
+
42
+ Args:
43
+ sql: The SQL statement that was executed
44
+ params: Parameters that were used
45
+ result: The query result
46
+
47
+ Returns:
48
+ Modified result (or original result)
49
+ """
50
+ return result
69
51
 
70
- def get_methods(self) -> Dict[str, Callable]:
71
- """Get all registered methods."""
72
- return self._methods.copy()
52
+ def on_connect(self, db_path: str, connection) -> None:
53
+ """Called when a database connection is established.
54
+
55
+ Args:
56
+ db_path: Path to the database file
57
+ connection: SQLite connection object
58
+ """
59
+ pass
73
60
 
74
- def call_hook(self, hook: PluginHook, *args, **kwargs) -> Any:
75
- """Call all callbacks for a specific hook."""
76
- results = []
77
- for callback in self._hooks.get(hook, []):
78
- try:
79
- result = callback(*args, **kwargs)
80
- results.append(result)
81
- except Exception as e:
82
- # Log error but don't break other plugins
83
- print(f"Plugin {self.name} hook {hook} failed: {e}")
84
- return results
61
+ def on_disconnect(self, db_path: str) -> None:
62
+ """Called when a database connection is closed.
63
+
64
+ Args:
65
+ db_path: Path to the database file
66
+ """
67
+ pass
85
68
 
86
69
  def cleanup(self) -> None:
87
- """Cleanup when plugin is unloaded."""
70
+ """Called when plugin is being unloaded."""
88
71
  pass
89
72
 
90
73
  @property
@@ -94,6 +77,4 @@ class BasePlugin(ABC):
94
77
  "name": self.name,
95
78
  "version": self.version,
96
79
  "description": self.description,
97
- "hooks": list(self._hooks.keys()),
98
- "methods": list(self._methods.keys()),
99
80
  }
@@ -1,45 +1,49 @@
1
1
  """
2
- Decorators for plugin development.
2
+ Simple decorators for plugin development.
3
3
  """
4
4
 
5
5
  from typing import Callable
6
6
 
7
- from .base import PluginHook
8
7
 
9
-
10
- def hook(hook_type: PluginHook):
11
- """Decorator to register a method as a hook callback."""
12
- def decorator(func: Callable) -> Callable:
13
- func._plugin_hook = hook_type
14
- return func
15
- return decorator
16
-
17
-
18
- def plugin_method(method_name: str):
19
- """Decorator to register a method to be added to CinchDB instances."""
8
+ def database_method(method_name: str):
9
+ """Decorator to mark a method for database extension.
10
+
11
+ Usage:
12
+ class Plugin:
13
+ @database_method("my_method")
14
+ def my_custom_method(self, db):
15
+ return "Hello from plugin!"
16
+ """
20
17
  def decorator(func: Callable) -> Callable:
21
- func._plugin_method_name = method_name
18
+ func._database_method_name = method_name
22
19
  return func
23
20
  return decorator
24
21
 
25
22
 
26
- class PluginDecorators:
27
- """Helper class to collect decorated methods from a plugin class."""
23
+ def auto_extend(plugin_class):
24
+ """Class decorator to automatically extend databases with decorated methods.
25
+
26
+ Usage:
27
+ @auto_extend
28
+ class Plugin:
29
+ @database_method("custom_query")
30
+ def custom_query_method(self, db, query):
31
+ # Method will be added to db instances as db.custom_query()
32
+ return "Custom result"
33
+ """
34
+ original_extend = getattr(plugin_class, 'extend_database', None)
28
35
 
29
- @staticmethod
30
- def collect_hooks(plugin_instance) -> None:
31
- """Collect and register hook methods from a plugin instance."""
32
- for attr_name in dir(plugin_instance):
33
- attr = getattr(plugin_instance, attr_name)
34
- if callable(attr) and hasattr(attr, '_plugin_hook'):
35
- hook_type = attr._plugin_hook
36
- plugin_instance.register_hook(hook_type, attr)
36
+ def new_extend_database(self, db):
37
+ # Call original extend_database if it exists
38
+ if original_extend:
39
+ original_extend(self, db)
40
+
41
+ # Auto-add decorated methods
42
+ for attr_name in dir(self):
43
+ attr = getattr(self, attr_name)
44
+ if callable(attr) and hasattr(attr, '_database_method_name'):
45
+ method_name = attr._database_method_name
46
+ setattr(db, method_name, lambda *args, **kwargs: attr(db, *args, **kwargs))
37
47
 
38
- @staticmethod
39
- def collect_methods(plugin_instance) -> None:
40
- """Collect and register plugin methods from a plugin instance."""
41
- for attr_name in dir(plugin_instance):
42
- attr = getattr(plugin_instance, attr_name)
43
- if callable(attr) and hasattr(attr, '_plugin_method_name'):
44
- method_name = attr._plugin_method_name
45
- plugin_instance.register_method(method_name, attr)
48
+ plugin_class.extend_database = new_extend_database
49
+ return plugin_class
@@ -1,43 +1,31 @@
1
1
  """
2
- Plugin manager for CinchDB.
2
+ Simple plugin manager for CinchDB.
3
3
  """
4
4
 
5
5
  import importlib
6
6
  import importlib.util
7
7
  import logging
8
8
  from pathlib import Path
9
- from typing import Dict, List, Optional, Any
10
- try:
11
- from importlib.metadata import entry_points
12
- except ImportError:
13
- # Fallback for Python < 3.8
14
- from importlib_metadata import entry_points
9
+ from typing import Dict, List, Optional, Any, Union
15
10
 
16
- from .base import BasePlugin, PluginHook
11
+ from .base import Plugin
17
12
 
18
13
  logger = logging.getLogger(__name__)
19
14
 
20
15
 
21
16
  class PluginManager:
22
- """Manages plugin lifecycle and hooks for CinchDB."""
17
+ """Simple plugin manager for CinchDB."""
23
18
 
24
19
  def __init__(self):
25
- self.plugins: Dict[str, BasePlugin] = {}
26
- self._cinchdb_instance = None
20
+ self.plugins: Dict[str, Plugin] = {}
21
+ self._database_instances: List[Any] = []
27
22
 
28
- def set_cinchdb_instance(self, instance):
29
- """Set the CinchDB instance for plugins."""
30
- self._cinchdb_instance = instance
31
-
32
- # Initialize any already loaded plugins
33
- for plugin in self.plugins.values():
34
- try:
35
- plugin.initialize(instance)
36
- except Exception as e:
37
- logger.error(f"Failed to initialize plugin {plugin.name}: {e}")
38
-
39
- def register_plugin(self, plugin: BasePlugin) -> None:
40
- """Register a plugin instance."""
23
+ def register_plugin(self, plugin: Union[Plugin, type]) -> None:
24
+ """Register a plugin instance or class."""
25
+ # If it's a class, instantiate it
26
+ if isinstance(plugin, type):
27
+ plugin = plugin()
28
+
41
29
  plugin_name = plugin.name
42
30
 
43
31
  if plugin_name in self.plugins:
@@ -45,13 +33,12 @@ class PluginManager:
45
33
 
46
34
  self.plugins[plugin_name] = plugin
47
35
 
48
- # Initialize with CinchDB instance if available
49
- if self._cinchdb_instance:
36
+ # Apply to existing database instances
37
+ for db_instance in self._database_instances:
50
38
  try:
51
- plugin.initialize(self._cinchdb_instance)
52
- self._apply_plugin_methods(plugin)
39
+ plugin.extend_database(db_instance)
53
40
  except Exception as e:
54
- logger.error(f"Failed to initialize plugin {plugin_name}: {e}")
41
+ logger.error(f"Failed to extend database with plugin {plugin_name}: {e}")
55
42
 
56
43
  logger.info(f"Plugin {plugin_name} registered successfully")
57
44
 
@@ -72,18 +59,24 @@ class PluginManager:
72
59
  try:
73
60
  module = importlib.import_module(module_name)
74
61
 
75
- # Look for plugin classes
62
+ # Look for Plugin class first
63
+ if hasattr(module, 'Plugin'):
64
+ plugin_instance = module.Plugin()
65
+ self.register_plugin(plugin_instance)
66
+ return
67
+
68
+ # Fallback: look for any Plugin subclass
76
69
  for attr_name in dir(module):
77
70
  attr = getattr(module, attr_name)
78
71
  if (isinstance(attr, type) and
79
- issubclass(attr, BasePlugin) and
80
- attr != BasePlugin):
72
+ issubclass(attr, Plugin) and
73
+ attr != Plugin):
81
74
 
82
75
  plugin_instance = attr()
83
76
  self.register_plugin(plugin_instance)
84
77
  return
85
78
 
86
- logger.warning(f"No plugin class found in module {module_name}")
79
+ logger.warning(f"No Plugin class found in module {module_name}")
87
80
 
88
81
  except ImportError as e:
89
82
  logger.error(f"Failed to import plugin module {module_name}: {e}")
@@ -98,69 +91,108 @@ class PluginManager:
98
91
  module = importlib.util.module_from_spec(spec)
99
92
  spec.loader.exec_module(module)
100
93
 
101
- # Look for plugin classes
102
- for attr_name in dir(module):
103
- attr = getattr(module, attr_name)
104
- if (isinstance(attr, type) and
105
- issubclass(attr, BasePlugin) and
106
- attr != BasePlugin):
107
-
108
- plugin_instance = attr()
109
- self.register_plugin(plugin_instance)
110
- return
94
+ # Look for Plugin class
95
+ if hasattr(module, 'Plugin'):
96
+ plugin_instance = module.Plugin()
97
+ self.register_plugin(plugin_instance)
98
+ return
111
99
 
112
- logger.warning(f"No plugin class found in file {file_path}")
100
+ logger.warning(f"No Plugin class found in file {file_path}")
113
101
 
114
102
  except Exception as e:
115
103
  logger.error(f"Failed to load plugin from file {file_path}: {e}")
116
104
 
105
+ def load_plugins_from_directory(self, plugins_dir: Path) -> None:
106
+ """Load all plugins from a directory."""
107
+ if not plugins_dir.exists():
108
+ logger.info(f"Plugins directory {plugins_dir} does not exist")
109
+ return
110
+
111
+ for plugin_file in plugins_dir.glob("*.py"):
112
+ if plugin_file.name == "__init__.py":
113
+ continue
114
+ self.load_plugin_from_file(plugin_file)
115
+
117
116
  def discover_plugins(self) -> None:
118
- """Discover plugins using entry points."""
117
+ """Discover plugins using entry points and plugins directory."""
118
+ # Try entry points for installed plugins
119
119
  try:
120
+ try:
121
+ from importlib.metadata import entry_points
122
+ except ImportError:
123
+ from importlib_metadata import entry_points
124
+
120
125
  eps = entry_points()
121
- # Handle both old and new entry_points API
122
126
  if hasattr(eps, 'select'):
123
- # New API (Python 3.10+)
124
127
  plugin_eps = eps.select(group='cinchdb.plugins')
125
128
  else:
126
- # Old API
127
129
  plugin_eps = eps.get('cinchdb.plugins', [])
128
130
 
129
131
  for entry_point in plugin_eps:
130
132
  try:
131
133
  plugin_class = entry_point.load()
132
- if issubclass(plugin_class, BasePlugin):
133
- plugin_instance = plugin_class()
134
- self.register_plugin(plugin_instance)
134
+ self.register_plugin(plugin_class)
135
135
  except Exception as e:
136
136
  logger.error(f"Failed to load plugin {entry_point.name}: {e}")
137
137
  except Exception as e:
138
- logger.error(f"Failed to discover plugins: {e}")
138
+ logger.debug(f"Entry points not available: {e}")
139
+
140
+ # Also check for local plugins directory
141
+ plugins_dir = Path("plugins")
142
+ if plugins_dir.exists():
143
+ self.load_plugins_from_directory(plugins_dir)
139
144
 
140
- def _apply_plugin_methods(self, plugin: BasePlugin) -> None:
141
- """Apply plugin methods to the CinchDB instance."""
142
- if not self._cinchdb_instance:
143
- return
144
-
145
- for method_name, method in plugin.get_methods().items():
146
- # Bind method to the instance
147
- bound_method = method.__get__(self._cinchdb_instance, type(self._cinchdb_instance))
148
- setattr(self._cinchdb_instance, method_name, bound_method)
149
-
150
- def call_hook(self, hook: PluginHook, *args, **kwargs) -> List[Any]:
151
- """Call all plugin hooks for a specific event."""
152
- results = []
145
+ def register_database(self, db_instance) -> None:
146
+ """Register a database instance with all plugins."""
147
+ self._database_instances.append(db_instance)
153
148
 
149
+ # Apply all plugins to this database instance
154
150
  for plugin in self.plugins.values():
155
151
  try:
156
- plugin_results = plugin.call_hook(hook, *args, **kwargs)
157
- results.extend(plugin_results)
152
+ plugin.extend_database(db_instance)
158
153
  except Exception as e:
159
- logger.error(f"Plugin {plugin.name} hook {hook} failed: {e}")
160
-
161
- return results
154
+ logger.error(f"Failed to extend database with plugin {plugin.name}: {e}")
155
+
156
+ def unregister_database(self, db_instance) -> None:
157
+ """Unregister a database instance."""
158
+ if db_instance in self._database_instances:
159
+ self._database_instances.remove(db_instance)
160
+
161
+ def before_query(self, sql: str, params: Optional[tuple] = None) -> tuple:
162
+ """Call before_query on all plugins."""
163
+ for plugin in self.plugins.values():
164
+ try:
165
+ sql, params = plugin.before_query(sql, params)
166
+ except Exception as e:
167
+ logger.error(f"Plugin {plugin.name} before_query failed: {e}")
168
+ return sql, params
169
+
170
+ def after_query(self, sql: str, params: Optional[tuple], result: Any) -> Any:
171
+ """Call after_query on all plugins."""
172
+ for plugin in self.plugins.values():
173
+ try:
174
+ result = plugin.after_query(sql, params, result)
175
+ except Exception as e:
176
+ logger.error(f"Plugin {plugin.name} after_query failed: {e}")
177
+ return result
178
+
179
+ def on_connect(self, db_path: str, connection) -> None:
180
+ """Call on_connect on all plugins."""
181
+ for plugin in self.plugins.values():
182
+ try:
183
+ plugin.on_connect(db_path, connection)
184
+ except Exception as e:
185
+ logger.error(f"Plugin {plugin.name} on_connect failed: {e}")
186
+
187
+ def on_disconnect(self, db_path: str) -> None:
188
+ """Call on_disconnect on all plugins."""
189
+ for plugin in self.plugins.values():
190
+ try:
191
+ plugin.on_disconnect(db_path)
192
+ except Exception as e:
193
+ logger.error(f"Plugin {plugin.name} on_disconnect failed: {e}")
162
194
 
163
- def get_plugin(self, name: str) -> Optional[BasePlugin]:
195
+ def get_plugin(self, name: str) -> Optional[Plugin]:
164
196
  """Get a plugin by name."""
165
197
  return self.plugins.get(name)
166
198
 
@@ -7,8 +7,9 @@ and follow consistent naming conventions.
7
7
  import re
8
8
 
9
9
 
10
- # Regex pattern for valid names: lowercase letters, numbers, dash, underscore, period
11
- VALID_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-_\.]*[a-z0-9]$|^[a-z0-9]$")
10
+ # Regex pattern for valid names: lowercase letters, numbers, dash, underscore
11
+ # Period removed to prevent directory traversal attempts like "../"
12
+ VALID_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9\-_]*[a-z0-9]$|^[a-z0-9]$")
12
13
 
13
14
  # Reserved names that cannot be used
14
15
  RESERVED_NAMES = {
@@ -52,6 +53,7 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
52
53
  - Be at least 1 character long
53
54
  - Not exceed 255 characters (filesystem limit)
54
55
  - Not be a reserved name
56
+ - Not contain path traversal sequences
55
57
 
56
58
  Args:
57
59
  name: The name to validate
@@ -67,6 +69,19 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
67
69
  raise InvalidNameError(
68
70
  f"{entity_type.capitalize()} name cannot exceed 255 characters"
69
71
  )
72
+
73
+ # Critical: Check for path traversal attempts
74
+ if ".." in name or "/" in name or "\\" in name or "~" in name:
75
+ raise InvalidNameError(
76
+ f"Security violation: {entity_type} name '{name}' contains "
77
+ f"forbidden path traversal characters"
78
+ )
79
+
80
+ # Check for null bytes and other control characters
81
+ if "\x00" in name or any(ord(c) < 32 for c in name):
82
+ raise InvalidNameError(
83
+ f"Security violation: {entity_type} name contains invalid control characters"
84
+ )
70
85
 
71
86
  # Check for lowercase requirement
72
87
  if name != name.lower():
@@ -80,7 +95,7 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
80
95
  raise InvalidNameError(
81
96
  f"Invalid {entity_type} name '{name}'. "
82
97
  f"Names must contain only lowercase letters (a-z), numbers (0-9), "
83
- f"dash (-), underscore (_), and period (.). "
98
+ f"dash (-), and underscore (_). "
84
99
  f"Names must start and end with alphanumeric characters."
85
100
  )
86
101
 
@@ -90,11 +105,6 @@ def validate_name(name: str, entity_type: str = "entity") -> None:
90
105
  or "__" in name
91
106
  or "-_" in name
92
107
  or "_-" in name
93
- or ".." in name
94
- or ".-" in name
95
- or "-." in name
96
- or "._" in name
97
- or "_." in name
98
108
  ):
99
109
  raise InvalidNameError(
100
110
  f"Invalid {entity_type} name '{name}'. "
@@ -132,14 +142,14 @@ def clean_name(name: str) -> str:
132
142
  # Replace spaces with dashes
133
143
  cleaned = cleaned.replace(" ", "-")
134
144
 
135
- # Remove invalid characters
136
- cleaned = re.sub(r"[^a-z0-9\-_\.]", "", cleaned)
145
+ # Remove invalid characters (period no longer allowed)
146
+ cleaned = re.sub(r"[^a-z0-9\-_]", "", cleaned)
137
147
 
138
148
  # Remove consecutive special characters
139
- cleaned = re.sub(r"[-_\.]{2,}", "-", cleaned)
149
+ cleaned = re.sub(r"[-_]{2,}", "-", cleaned)
140
150
 
141
151
  # Remove leading/trailing special characters
142
- cleaned = cleaned.strip("-_.")
152
+ cleaned = cleaned.strip("-_")
143
153
 
144
154
  return cleaned
145
155
 
@@ -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