cinchdb 0.1.5__tar.gz → 0.1.7__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.
- {cinchdb-0.1.5 → cinchdb-0.1.7}/PKG-INFO +24 -6
- {cinchdb-0.1.5 → cinchdb-0.1.7}/README.md +23 -5
- {cinchdb-0.1.5 → cinchdb-0.1.7}/pyproject.toml +1 -1
- cinchdb-0.1.7/src/cinchdb/cli/commands/__init__.py +16 -0
- cinchdb-0.1.7/src/cinchdb/cli/commands/index.py +186 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/main.py +2 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/database.py +108 -14
- cinchdb-0.1.7/src/cinchdb/managers/index.py +300 -0
- cinchdb-0.1.5/src/cinchdb/cli/commands/__init__.py +0 -1
- {cinchdb-0.1.5 → cinchdb-0.1.7}/.gitignore +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/LICENSE +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/__main__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/branch.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/codegen.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/column.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/database.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/query.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/remote.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/table.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/tenant.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/commands/view.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/handlers/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/handlers/codegen_handler.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/cli/utils.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/config.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/connection.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/initializer.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/maintenance.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/core/path_utils.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/branch.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/change_applier.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/change_comparator.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/change_tracker.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/codegen.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/column.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/data.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/merge_manager.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/query.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/table.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/tenant.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/managers/view.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/base.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/branch.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/change.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/database.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/project.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/table.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/tenant.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/models/view.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/utils/__init__.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/src/cinchdb/utils/name_validator.py +0 -0
- {cinchdb-0.1.5 → cinchdb-0.1.7}/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.
|
3
|
+
Version: 0.1.7
|
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
|
@@ -31,9 +31,9 @@ Description-Content-Type: text/markdown
|
|
31
31
|
|
32
32
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
33
33
|
|
34
|
-
CinchDB is for projects that need fast queries,
|
34
|
+
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
35
|
|
36
|
-
On a meta level
|
36
|
+
On a meta level: I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
37
37
|
|
38
38
|
Because it's so lightweight and its only dependencies are pydantic, requests, and Typer, it makes for a perfect local development database that can be controlled programmatically.
|
39
39
|
|
@@ -58,6 +58,7 @@ cinch branch merge-into-main feature
|
|
58
58
|
cinch tenant create customer_a
|
59
59
|
cinch query "SELECT * FROM users" --tenant customer_a
|
60
60
|
|
61
|
+
# Coming soon
|
61
62
|
# Connect to remote CinchDB instance
|
62
63
|
cinch remote add production https://your-cinchdb-server.com your-api-key
|
63
64
|
cinch remote use production
|
@@ -76,7 +77,7 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
|
|
76
77
|
- **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
|
77
78
|
- **Remote connectivity** - Connect to hosted CinchDB instances
|
78
79
|
- **Type-safe SDK** - Python and TypeScript SDKs with full type safety
|
79
|
-
- **Remote-capable** - CLI and SDK can connect to remote instances
|
80
|
+
- **Remote-capable** - coming soon - CLI and SDK can connect to remote instances
|
80
81
|
- **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
|
81
82
|
|
82
83
|
## Installation
|
@@ -128,13 +129,30 @@ db.create_table("posts", [
|
|
128
129
|
# Query data
|
129
130
|
results = db.query("SELECT * FROM posts WHERE title LIKE ?", ["%python%"])
|
130
131
|
|
131
|
-
# CRUD operations
|
132
|
+
# CRUD operations - single insert
|
132
133
|
post_id = db.insert("posts", {"title": "Hello World", "content": "First post"})
|
134
|
+
|
135
|
+
# Batch insert - multiple records at once
|
136
|
+
posts = db.insert("posts",
|
137
|
+
{"title": "First", "content": "Content 1"},
|
138
|
+
{"title": "Second", "content": "Content 2"},
|
139
|
+
{"title": "Third", "content": "Content 3"}
|
140
|
+
)
|
141
|
+
|
142
|
+
# Or with a list using star expansion
|
143
|
+
post_list = [
|
144
|
+
{"title": "Post A", "content": "Content A"},
|
145
|
+
{"title": "Post B", "content": "Content B"}
|
146
|
+
]
|
147
|
+
results = db.insert("posts", *post_list)
|
148
|
+
|
133
149
|
db.update("posts", post_id, {"content": "Updated content"})
|
134
150
|
```
|
135
151
|
|
136
152
|
### Remote Connection
|
137
153
|
|
154
|
+
Coming soon.
|
155
|
+
|
138
156
|
```python
|
139
157
|
# Connect to remote instance
|
140
158
|
db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
|
@@ -144,7 +162,7 @@ results = db.query("SELECT * FROM users")
|
|
144
162
|
user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
|
145
163
|
```
|
146
164
|
|
147
|
-
## Remote Access
|
165
|
+
## Remote Access - coming soon
|
148
166
|
|
149
167
|
Connect to a remote CinchDB instance:
|
150
168
|
|
@@ -4,9 +4,9 @@
|
|
4
4
|
|
5
5
|
NOTE: CinchDB is in early alpha. This is project to test out an idea. Do not use this in production.
|
6
6
|
|
7
|
-
CinchDB is for projects that need fast queries,
|
7
|
+
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.
|
8
8
|
|
9
|
-
On a meta level
|
9
|
+
On a meta level: I made this because I wanted a database structure that I felt comfortable letting AI agents take full control over, safely, and I didn't want to run my own Postgres instance somewhere or pay for it on e.g. Neon - I don't need hyperscaling, I just need super fast queries.
|
10
10
|
|
11
11
|
Because it's so lightweight and its only dependencies are pydantic, requests, and Typer, it makes for a perfect local development database that can be controlled programmatically.
|
12
12
|
|
@@ -31,6 +31,7 @@ cinch branch merge-into-main feature
|
|
31
31
|
cinch tenant create customer_a
|
32
32
|
cinch query "SELECT * FROM users" --tenant customer_a
|
33
33
|
|
34
|
+
# Coming soon
|
34
35
|
# Connect to remote CinchDB instance
|
35
36
|
cinch remote add production https://your-cinchdb-server.com your-api-key
|
36
37
|
cinch remote use production
|
@@ -49,7 +50,7 @@ CinchDB combines SQLite with Git-like workflows for database schema management:
|
|
49
50
|
- **Safe structure changes** - change merges happen atomically with zero rollback risk (seriously)
|
50
51
|
- **Remote connectivity** - Connect to hosted CinchDB instances
|
51
52
|
- **Type-safe SDK** - Python and TypeScript SDKs with full type safety
|
52
|
-
- **Remote-capable** - CLI and SDK can connect to remote instances
|
53
|
+
- **Remote-capable** - coming soon - CLI and SDK can connect to remote instances
|
53
54
|
- **SDK generation from database schema** - Generate a typesafe SDK from your database models for CRUD operations
|
54
55
|
|
55
56
|
## Installation
|
@@ -101,13 +102,30 @@ db.create_table("posts", [
|
|
101
102
|
# Query data
|
102
103
|
results = db.query("SELECT * FROM posts WHERE title LIKE ?", ["%python%"])
|
103
104
|
|
104
|
-
# CRUD operations
|
105
|
+
# CRUD operations - single insert
|
105
106
|
post_id = db.insert("posts", {"title": "Hello World", "content": "First post"})
|
107
|
+
|
108
|
+
# Batch insert - multiple records at once
|
109
|
+
posts = db.insert("posts",
|
110
|
+
{"title": "First", "content": "Content 1"},
|
111
|
+
{"title": "Second", "content": "Content 2"},
|
112
|
+
{"title": "Third", "content": "Content 3"}
|
113
|
+
)
|
114
|
+
|
115
|
+
# Or with a list using star expansion
|
116
|
+
post_list = [
|
117
|
+
{"title": "Post A", "content": "Content A"},
|
118
|
+
{"title": "Post B", "content": "Content B"}
|
119
|
+
]
|
120
|
+
results = db.insert("posts", *post_list)
|
121
|
+
|
106
122
|
db.update("posts", post_id, {"content": "Updated content"})
|
107
123
|
```
|
108
124
|
|
109
125
|
### Remote Connection
|
110
126
|
|
127
|
+
Coming soon.
|
128
|
+
|
111
129
|
```python
|
112
130
|
# Connect to remote instance
|
113
131
|
db = cinchdb.connect("myapp", url="https://your-cinchdb-server.com", api_key="your-api-key")
|
@@ -117,7 +135,7 @@ results = db.query("SELECT * FROM users")
|
|
117
135
|
user_id = db.insert("users", {"username": "alice", "email": "alice@example.com"})
|
118
136
|
```
|
119
137
|
|
120
|
-
## Remote Access
|
138
|
+
## Remote Access - coming soon
|
121
139
|
|
122
140
|
Connect to a remote CinchDB instance:
|
123
141
|
|
@@ -0,0 +1,16 @@
|
|
1
|
+
"""CLI command modules."""
|
2
|
+
|
3
|
+
from . import database, branch, tenant, table, column, view, query, codegen, remote, index
|
4
|
+
|
5
|
+
__all__ = [
|
6
|
+
"database",
|
7
|
+
"branch",
|
8
|
+
"tenant",
|
9
|
+
"table",
|
10
|
+
"column",
|
11
|
+
"view",
|
12
|
+
"query",
|
13
|
+
"codegen",
|
14
|
+
"remote",
|
15
|
+
"index",
|
16
|
+
]
|
@@ -0,0 +1,186 @@
|
|
1
|
+
"""Index management commands for CinchDB CLI."""
|
2
|
+
|
3
|
+
import typer
|
4
|
+
from typing import List, Optional
|
5
|
+
from rich import print
|
6
|
+
from rich.table import Table
|
7
|
+
from rich.console import Console
|
8
|
+
|
9
|
+
from cinchdb.config import Config
|
10
|
+
from cinchdb.managers.index import IndexManager
|
11
|
+
from cinchdb.cli.utils import handle_cli_error
|
12
|
+
|
13
|
+
app = typer.Typer(help="Manage database indexes")
|
14
|
+
console = Console()
|
15
|
+
|
16
|
+
|
17
|
+
@app.command("create")
|
18
|
+
@handle_cli_error
|
19
|
+
def create_index(
|
20
|
+
table: str = typer.Argument(..., help="Table name"),
|
21
|
+
columns: List[str] = typer.Argument(..., help="Column names to index"),
|
22
|
+
name: Optional[str] = typer.Option(None, "--name", "-n", help="Index name"),
|
23
|
+
unique: bool = typer.Option(False, "--unique", "-u", help="Create unique index"),
|
24
|
+
database: Optional[str] = typer.Option(None, "--database", "-d", help="Database name"),
|
25
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch name"),
|
26
|
+
):
|
27
|
+
"""Create an index on a table.
|
28
|
+
|
29
|
+
Indexes are created at the branch level and apply to all tenants.
|
30
|
+
|
31
|
+
Examples:
|
32
|
+
cinch index create users email
|
33
|
+
cinch index create orders user_id created_at --name idx_user_orders
|
34
|
+
cinch index create products sku --unique
|
35
|
+
"""
|
36
|
+
config = Config()
|
37
|
+
project_config = config.load()
|
38
|
+
|
39
|
+
# Use provided values or defaults
|
40
|
+
database = database or project_config.active_database
|
41
|
+
branch = branch or project_config.active_branch
|
42
|
+
|
43
|
+
manager = IndexManager(config.base_dir, database, branch)
|
44
|
+
|
45
|
+
try:
|
46
|
+
index_name = manager.create_index(table, columns, name, unique)
|
47
|
+
|
48
|
+
unique_text = "[green]UNIQUE[/green] " if unique else ""
|
49
|
+
columns_text = ", ".join(columns)
|
50
|
+
print(f"✓ Created {unique_text}index [bold cyan]{index_name}[/bold cyan] on {table}({columns_text})")
|
51
|
+
|
52
|
+
except ValueError as e:
|
53
|
+
print(f"[red]✗ Error:[/red] {e}")
|
54
|
+
raise typer.Exit(1)
|
55
|
+
|
56
|
+
|
57
|
+
@app.command("drop")
|
58
|
+
@handle_cli_error
|
59
|
+
def drop_index(
|
60
|
+
name: str = typer.Argument(..., help="Index name"),
|
61
|
+
database: Optional[str] = typer.Option(None, "--database", "-d", help="Database name"),
|
62
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch name"),
|
63
|
+
):
|
64
|
+
"""Drop an index.
|
65
|
+
|
66
|
+
Indexes are managed at the branch level.
|
67
|
+
|
68
|
+
Example:
|
69
|
+
cinch index drop idx_users_email
|
70
|
+
"""
|
71
|
+
config = Config()
|
72
|
+
project_config = config.load()
|
73
|
+
|
74
|
+
# Use provided values or defaults
|
75
|
+
database = database or project_config.active_database
|
76
|
+
branch = branch or project_config.active_branch
|
77
|
+
|
78
|
+
manager = IndexManager(config.base_dir, database, branch)
|
79
|
+
|
80
|
+
try:
|
81
|
+
manager.drop_index(name)
|
82
|
+
print(f"✓ Dropped index [bold cyan]{name}[/bold cyan]")
|
83
|
+
except ValueError as e:
|
84
|
+
print(f"[red]✗ Error:[/red] {e}")
|
85
|
+
raise typer.Exit(1)
|
86
|
+
|
87
|
+
|
88
|
+
@app.command("list")
|
89
|
+
@handle_cli_error
|
90
|
+
def list_indexes(
|
91
|
+
table: Optional[str] = typer.Argument(None, help="Table name to filter indexes"),
|
92
|
+
database: Optional[str] = typer.Option(None, "--database", "-d", help="Database name"),
|
93
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch name"),
|
94
|
+
):
|
95
|
+
"""List indexes for a table or all tables.
|
96
|
+
|
97
|
+
Indexes are managed at the branch level and apply to all tenants.
|
98
|
+
|
99
|
+
Examples:
|
100
|
+
cinch index list
|
101
|
+
cinch index list users
|
102
|
+
"""
|
103
|
+
config = Config()
|
104
|
+
project_config = config.load()
|
105
|
+
|
106
|
+
# Use provided values or defaults
|
107
|
+
database = database or project_config.active_database
|
108
|
+
branch = branch or project_config.active_branch
|
109
|
+
|
110
|
+
manager = IndexManager(config.base_dir, database, branch)
|
111
|
+
|
112
|
+
indexes = manager.list_indexes(table)
|
113
|
+
|
114
|
+
if not indexes:
|
115
|
+
if table:
|
116
|
+
print(f"No indexes found for table [cyan]{table}[/cyan]")
|
117
|
+
else:
|
118
|
+
print("No indexes found")
|
119
|
+
return
|
120
|
+
|
121
|
+
# Create table for display
|
122
|
+
table_obj = Table(title=f"Indexes{f' for {table}' if table else ''}")
|
123
|
+
table_obj.add_column("Name", style="cyan")
|
124
|
+
table_obj.add_column("Table", style="yellow")
|
125
|
+
table_obj.add_column("Columns", style="green")
|
126
|
+
table_obj.add_column("Unique", style="magenta")
|
127
|
+
|
128
|
+
for idx in indexes:
|
129
|
+
columns_str = ", ".join(idx["columns"])
|
130
|
+
unique_str = "✓" if idx["unique"] else ""
|
131
|
+
table_obj.add_row(
|
132
|
+
idx["name"],
|
133
|
+
idx["table"],
|
134
|
+
columns_str,
|
135
|
+
unique_str
|
136
|
+
)
|
137
|
+
|
138
|
+
console.print(table_obj)
|
139
|
+
|
140
|
+
|
141
|
+
@app.command("info")
|
142
|
+
@handle_cli_error
|
143
|
+
def index_info(
|
144
|
+
name: str = typer.Argument(..., help="Index name"),
|
145
|
+
database: Optional[str] = typer.Option(None, "--database", "-d", help="Database name"),
|
146
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch name"),
|
147
|
+
):
|
148
|
+
"""Show detailed information about an index.
|
149
|
+
|
150
|
+
Example:
|
151
|
+
cinch index info idx_users_email
|
152
|
+
"""
|
153
|
+
config = Config()
|
154
|
+
project_config = config.load()
|
155
|
+
|
156
|
+
# Use provided values or defaults
|
157
|
+
database = database or project_config.active_database
|
158
|
+
branch = branch or project_config.active_branch
|
159
|
+
|
160
|
+
manager = IndexManager(config.base_dir, database, branch)
|
161
|
+
|
162
|
+
try:
|
163
|
+
info = manager.get_index_info(name)
|
164
|
+
|
165
|
+
print(f"\nIndex: [bold cyan]{info['name']}[/bold cyan]")
|
166
|
+
print(f"Table: [yellow]{info['table']}[/yellow]")
|
167
|
+
print(f"Columns: [green]{', '.join(info['columns'])}[/green]")
|
168
|
+
print(f"Unique: [magenta]{'Yes' if info['unique'] else 'No'}[/magenta]")
|
169
|
+
print(f"Partial: [blue]{'Yes' if info.get('partial') else 'No'}[/blue]")
|
170
|
+
|
171
|
+
if info.get('sql'):
|
172
|
+
print(f"\nSQL Definition:")
|
173
|
+
print(f"[dim]{info['sql']}[/dim]")
|
174
|
+
|
175
|
+
if info.get('columns_info'):
|
176
|
+
print(f"\nColumn Details:")
|
177
|
+
for col in info['columns_info']:
|
178
|
+
print(f" - Position {col['position']}: {col['column_name']}")
|
179
|
+
|
180
|
+
except ValueError as e:
|
181
|
+
print(f"[red]✗ Error:[/red] {e}")
|
182
|
+
raise typer.Exit(1)
|
183
|
+
|
184
|
+
|
185
|
+
if __name__ == "__main__":
|
186
|
+
app()
|
@@ -14,6 +14,7 @@ from cinchdb.cli.commands import (
|
|
14
14
|
view,
|
15
15
|
codegen,
|
16
16
|
remote,
|
17
|
+
index,
|
17
18
|
)
|
18
19
|
|
19
20
|
app = typer.Typer(
|
@@ -42,6 +43,7 @@ app.add_typer(tenant.app, name="tenant", help="Tenant management commands")
|
|
42
43
|
app.add_typer(table.app, name="table", help="Table management commands")
|
43
44
|
app.add_typer(column.app, name="column", help="Column management commands")
|
44
45
|
app.add_typer(view.app, name="view", help="View management commands")
|
46
|
+
app.add_typer(index.app, name="index", help="Index management commands")
|
45
47
|
app.add_typer(codegen.app, name="codegen", help="Code generation commands")
|
46
48
|
app.add_typer(remote.app, name="remote", help="Remote instance management")
|
47
49
|
|
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
|
|
17
17
|
from cinchdb.managers.tenant import TenantManager
|
18
18
|
from cinchdb.managers.codegen import CodegenManager
|
19
19
|
from cinchdb.managers.merge_manager import MergeManager
|
20
|
+
from cinchdb.managers.index import IndexManager
|
20
21
|
|
21
22
|
|
22
23
|
class CinchDB:
|
@@ -108,6 +109,7 @@ class CinchDB:
|
|
108
109
|
self._tenant_manager: Optional["TenantManager"] = None
|
109
110
|
self._codegen_manager: Optional["CodegenManager"] = None
|
110
111
|
self._merge_manager: Optional["MergeManager"] = None
|
112
|
+
self._index_manager: Optional["IndexManager"] = None
|
111
113
|
|
112
114
|
@property
|
113
115
|
def session(self):
|
@@ -303,6 +305,21 @@ class CinchDB:
|
|
303
305
|
self._merge_manager = MergeManager(self.project_dir, self.database)
|
304
306
|
return self._merge_manager
|
305
307
|
|
308
|
+
@property
|
309
|
+
def indexes(self) -> "IndexManager":
|
310
|
+
"""Access index operations (local only)."""
|
311
|
+
if not self.is_local:
|
312
|
+
raise RuntimeError(
|
313
|
+
"Direct manager access not available for remote connections"
|
314
|
+
)
|
315
|
+
if self._index_manager is None:
|
316
|
+
from cinchdb.managers.index import IndexManager
|
317
|
+
|
318
|
+
self._index_manager = IndexManager(
|
319
|
+
self.project_dir, self.database, self.branch
|
320
|
+
)
|
321
|
+
return self._index_manager
|
322
|
+
|
306
323
|
# Convenience methods for common operations
|
307
324
|
|
308
325
|
def query(
|
@@ -363,23 +380,37 @@ class CinchDB:
|
|
363
380
|
"POST", "/tables", json={"name": name, "columns": columns_data}
|
364
381
|
)
|
365
382
|
|
366
|
-
def insert(self, table: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
367
|
-
"""Insert
|
383
|
+
def insert(self, table: str, *data: Dict[str, Any]) -> Dict[str, Any] | List[Dict[str, Any]]:
|
384
|
+
"""Insert one or more records into a table.
|
368
385
|
|
369
386
|
Args:
|
370
387
|
table: Table name
|
371
|
-
data:
|
388
|
+
*data: One or more record data dictionaries
|
372
389
|
|
373
390
|
Returns:
|
374
|
-
|
391
|
+
Single record dict if one record inserted, list of dicts if multiple
|
375
392
|
|
376
393
|
Examples:
|
377
|
-
#
|
394
|
+
# Single insert
|
378
395
|
db.insert("users", {"name": "John", "email": "john@example.com"})
|
379
396
|
|
380
|
-
#
|
381
|
-
db.insert("
|
397
|
+
# Multiple inserts using star expansion
|
398
|
+
db.insert("users",
|
399
|
+
{"name": "John", "email": "john@example.com"},
|
400
|
+
{"name": "Jane", "email": "jane@example.com"},
|
401
|
+
{"name": "Bob", "email": "bob@example.com"}
|
402
|
+
)
|
403
|
+
|
404
|
+
# Or with a list using star expansion
|
405
|
+
users = [
|
406
|
+
{"name": "Alice", "email": "alice@example.com"},
|
407
|
+
{"name": "Charlie", "email": "charlie@example.com"}
|
408
|
+
]
|
409
|
+
db.insert("users", *users)
|
382
410
|
"""
|
411
|
+
if not data:
|
412
|
+
raise ValueError("At least one record must be provided")
|
413
|
+
|
383
414
|
if self.is_local:
|
384
415
|
# Initialize data manager if needed
|
385
416
|
if self._data_manager is None:
|
@@ -387,14 +418,31 @@ class CinchDB:
|
|
387
418
|
self._data_manager = DataManager(
|
388
419
|
self.project_dir, self.database, self.branch, self.tenant
|
389
420
|
)
|
390
|
-
|
391
|
-
|
421
|
+
|
422
|
+
# Single record
|
423
|
+
if len(data) == 1:
|
424
|
+
return self._data_manager.create_from_dict(table, data[0])
|
425
|
+
|
426
|
+
# Multiple records - batch insert
|
427
|
+
results = []
|
428
|
+
for record in data:
|
429
|
+
result = self._data_manager.create_from_dict(table, record)
|
430
|
+
results.append(result)
|
431
|
+
return results
|
392
432
|
else:
|
393
|
-
# Remote insert
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
433
|
+
# Remote insert
|
434
|
+
if len(data) == 1:
|
435
|
+
# Single record - use existing endpoint
|
436
|
+
result = self._make_request(
|
437
|
+
"POST", f"/tables/{table}/data", json={"data": data[0]}
|
438
|
+
)
|
439
|
+
return result
|
440
|
+
else:
|
441
|
+
# Multiple records - use bulk endpoint
|
442
|
+
result = self._make_request(
|
443
|
+
"POST", f"/tables/{table}/data/bulk", json={"records": list(data)}
|
444
|
+
)
|
445
|
+
return result
|
398
446
|
|
399
447
|
def update(self, table: str, id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
400
448
|
"""Update a record in a table.
|
@@ -429,6 +477,52 @@ class CinchDB:
|
|
429
477
|
# Remote delete
|
430
478
|
self._make_request("DELETE", f"/tables/{table}/data/{id}")
|
431
479
|
|
480
|
+
def create_index(
|
481
|
+
self,
|
482
|
+
table: str,
|
483
|
+
columns: List[str],
|
484
|
+
name: Optional[str] = None,
|
485
|
+
unique: bool = False,
|
486
|
+
) -> str:
|
487
|
+
"""Create an index on a table at the branch level.
|
488
|
+
|
489
|
+
Indexes are created for the current branch and apply to all tenants.
|
490
|
+
|
491
|
+
Args:
|
492
|
+
table: Table name
|
493
|
+
columns: List of column names to index
|
494
|
+
name: Optional index name (auto-generated if not provided)
|
495
|
+
unique: Whether to create a unique index
|
496
|
+
|
497
|
+
Returns:
|
498
|
+
str: Name of the created index
|
499
|
+
|
500
|
+
Examples:
|
501
|
+
# Simple index on one column
|
502
|
+
db.create_index("users", ["email"])
|
503
|
+
|
504
|
+
# Unique compound index
|
505
|
+
db.create_index("orders", ["user_id", "order_number"], unique=True)
|
506
|
+
|
507
|
+
# Named index
|
508
|
+
db.create_index("products", ["category", "price"], name="idx_category_price")
|
509
|
+
"""
|
510
|
+
if self.is_local:
|
511
|
+
return self.indexes.create_index(table, columns, name, unique)
|
512
|
+
else:
|
513
|
+
# Remote index creation
|
514
|
+
result = self._make_request(
|
515
|
+
"POST",
|
516
|
+
"/indexes",
|
517
|
+
json={
|
518
|
+
"table": table,
|
519
|
+
"columns": columns,
|
520
|
+
"name": name,
|
521
|
+
"unique": unique,
|
522
|
+
},
|
523
|
+
)
|
524
|
+
return result.get("name")
|
525
|
+
|
432
526
|
def list_changes(self) -> List["Change"]:
|
433
527
|
"""List all changes for the current branch.
|
434
528
|
|
@@ -0,0 +1,300 @@
|
|
1
|
+
"""Index management for CinchDB."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import List, Dict, Any, Optional
|
5
|
+
import sqlite3
|
6
|
+
import json
|
7
|
+
from datetime import datetime, timezone
|
8
|
+
import uuid
|
9
|
+
|
10
|
+
from cinchdb.core.connection import DatabaseConnection
|
11
|
+
from cinchdb.core.path_utils import get_tenant_db_path
|
12
|
+
from cinchdb.models.change import Change, ChangeType
|
13
|
+
|
14
|
+
|
15
|
+
class IndexManager:
|
16
|
+
"""Manages database indexes for CinchDB tables at the branch level."""
|
17
|
+
|
18
|
+
def __init__(
|
19
|
+
self, project_dir: Path, database: str, branch: str
|
20
|
+
):
|
21
|
+
"""Initialize IndexManager.
|
22
|
+
|
23
|
+
Args:
|
24
|
+
project_dir: Path to the project directory
|
25
|
+
database: Database name
|
26
|
+
branch: Branch name
|
27
|
+
"""
|
28
|
+
self.project_dir = Path(project_dir)
|
29
|
+
self.database = database
|
30
|
+
self.branch = branch
|
31
|
+
|
32
|
+
def create_index(
|
33
|
+
self,
|
34
|
+
table: str,
|
35
|
+
columns: List[str],
|
36
|
+
name: Optional[str] = None,
|
37
|
+
unique: bool = False,
|
38
|
+
if_not_exists: bool = True,
|
39
|
+
) -> str:
|
40
|
+
"""Create an index on a table.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
table: Table name
|
44
|
+
columns: List of column names to index
|
45
|
+
name: Optional index name (auto-generated if not provided)
|
46
|
+
unique: Whether to create a unique index
|
47
|
+
if_not_exists: Whether to use IF NOT EXISTS clause
|
48
|
+
|
49
|
+
Returns:
|
50
|
+
str: Name of the created index
|
51
|
+
|
52
|
+
Raises:
|
53
|
+
ValueError: If table doesn't exist or columns are invalid
|
54
|
+
"""
|
55
|
+
if not columns:
|
56
|
+
raise ValueError("At least one column must be specified for the index")
|
57
|
+
|
58
|
+
# Generate index name if not provided
|
59
|
+
if not name:
|
60
|
+
column_str = "_".join(columns)
|
61
|
+
unique_prefix = "uniq_" if unique else "idx_"
|
62
|
+
name = f"{unique_prefix}{table}_{column_str}"
|
63
|
+
|
64
|
+
# Get connection to main tenant database (indexes are branch-level)
|
65
|
+
db_path = get_tenant_db_path(
|
66
|
+
self.project_dir, self.database, self.branch, "main"
|
67
|
+
)
|
68
|
+
|
69
|
+
with DatabaseConnection(db_path) as conn:
|
70
|
+
# Verify table exists
|
71
|
+
result = conn.execute(
|
72
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
73
|
+
[table]
|
74
|
+
)
|
75
|
+
if not result.fetchone():
|
76
|
+
raise ValueError(f"Table '{table}' does not exist")
|
77
|
+
|
78
|
+
# Verify columns exist
|
79
|
+
result = conn.execute(f"PRAGMA table_info({table})")
|
80
|
+
existing_columns = {row[1] for row in result.fetchall()}
|
81
|
+
|
82
|
+
invalid_columns = set(columns) - existing_columns
|
83
|
+
if invalid_columns:
|
84
|
+
raise ValueError(
|
85
|
+
f"Columns {invalid_columns} do not exist in table '{table}'"
|
86
|
+
)
|
87
|
+
|
88
|
+
# Build and execute CREATE INDEX statement
|
89
|
+
unique_clause = "UNIQUE " if unique else ""
|
90
|
+
if_not_exists_clause = "IF NOT EXISTS " if if_not_exists else ""
|
91
|
+
column_list = ", ".join(columns)
|
92
|
+
|
93
|
+
sql = f"CREATE {unique_clause}INDEX {if_not_exists_clause}{name} ON {table} ({column_list})"
|
94
|
+
|
95
|
+
try:
|
96
|
+
result = conn.execute(sql)
|
97
|
+
conn.commit()
|
98
|
+
except sqlite3.Error as e:
|
99
|
+
if "already exists" in str(e):
|
100
|
+
if not if_not_exists:
|
101
|
+
raise ValueError(f"Index '{name}' already exists")
|
102
|
+
else:
|
103
|
+
raise
|
104
|
+
|
105
|
+
# Track the change
|
106
|
+
self._track_change(
|
107
|
+
ChangeType.CREATE_INDEX,
|
108
|
+
name,
|
109
|
+
{"table": table, "columns": columns, "unique": unique}
|
110
|
+
)
|
111
|
+
|
112
|
+
return name
|
113
|
+
|
114
|
+
def drop_index(self, name: str, if_exists: bool = True) -> None:
|
115
|
+
"""Drop an index.
|
116
|
+
|
117
|
+
Args:
|
118
|
+
name: Index name
|
119
|
+
if_exists: Whether to use IF EXISTS clause
|
120
|
+
|
121
|
+
Raises:
|
122
|
+
ValueError: If index doesn't exist and if_exists is False
|
123
|
+
"""
|
124
|
+
# Get connection to main tenant database (indexes are branch-level)
|
125
|
+
db_path = get_tenant_db_path(
|
126
|
+
self.project_dir, self.database, self.branch, "main"
|
127
|
+
)
|
128
|
+
|
129
|
+
with DatabaseConnection(db_path) as conn:
|
130
|
+
|
131
|
+
# Check if index exists
|
132
|
+
result = conn.execute(
|
133
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
134
|
+
[name]
|
135
|
+
)
|
136
|
+
exists = result.fetchone() is not None
|
137
|
+
|
138
|
+
if not exists and not if_exists:
|
139
|
+
raise ValueError(f"Index '{name}' does not exist")
|
140
|
+
|
141
|
+
if exists:
|
142
|
+
if_exists_clause = "IF EXISTS " if if_exists else ""
|
143
|
+
sql = f"DROP INDEX {if_exists_clause}{name}"
|
144
|
+
|
145
|
+
result = conn.execute(sql)
|
146
|
+
conn.commit()
|
147
|
+
|
148
|
+
# Track the change
|
149
|
+
self._track_change(ChangeType.DROP_INDEX, name, {})
|
150
|
+
|
151
|
+
def list_indexes(self, table: Optional[str] = None) -> List[Dict[str, Any]]:
|
152
|
+
"""List indexes for a table or all tables.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
table: Optional table name to filter indexes
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
List of index information dictionaries
|
159
|
+
"""
|
160
|
+
# Get connection to main tenant database (indexes are branch-level)
|
161
|
+
db_path = get_tenant_db_path(
|
162
|
+
self.project_dir, self.database, self.branch, "main"
|
163
|
+
)
|
164
|
+
|
165
|
+
indexes = []
|
166
|
+
|
167
|
+
with DatabaseConnection(db_path) as conn:
|
168
|
+
|
169
|
+
# Get all indexes (excluding SQLite internal indexes)
|
170
|
+
if table:
|
171
|
+
result = conn.execute(
|
172
|
+
"""
|
173
|
+
SELECT name, tbl_name, sql
|
174
|
+
FROM sqlite_master
|
175
|
+
WHERE type='index'
|
176
|
+
AND tbl_name=?
|
177
|
+
AND sql IS NOT NULL
|
178
|
+
""",
|
179
|
+
[table]
|
180
|
+
)
|
181
|
+
else:
|
182
|
+
result = conn.execute(
|
183
|
+
"""
|
184
|
+
SELECT name, tbl_name, sql
|
185
|
+
FROM sqlite_master
|
186
|
+
WHERE type='index'
|
187
|
+
AND sql IS NOT NULL
|
188
|
+
"""
|
189
|
+
)
|
190
|
+
|
191
|
+
for row in result.fetchall():
|
192
|
+
index_name, table_name, sql = row
|
193
|
+
|
194
|
+
# Parse unique from SQL
|
195
|
+
is_unique = "CREATE UNIQUE INDEX" in sql.upper()
|
196
|
+
|
197
|
+
# Get indexed columns
|
198
|
+
pragma_result = conn.execute(f"PRAGMA index_info({index_name})")
|
199
|
+
columns = [info[2] for info in pragma_result.fetchall()]
|
200
|
+
|
201
|
+
indexes.append({
|
202
|
+
"name": index_name,
|
203
|
+
"table": table_name,
|
204
|
+
"columns": columns,
|
205
|
+
"unique": is_unique,
|
206
|
+
"sql": sql
|
207
|
+
})
|
208
|
+
|
209
|
+
return indexes
|
210
|
+
|
211
|
+
def get_index_info(self, name: str) -> Dict[str, Any]:
|
212
|
+
"""Get detailed information about a specific index.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
name: Index name
|
216
|
+
|
217
|
+
Returns:
|
218
|
+
Dictionary with index information
|
219
|
+
|
220
|
+
Raises:
|
221
|
+
ValueError: If index doesn't exist
|
222
|
+
"""
|
223
|
+
# Get connection to main tenant database (indexes are branch-level)
|
224
|
+
db_path = get_tenant_db_path(
|
225
|
+
self.project_dir, self.database, self.branch, "main"
|
226
|
+
)
|
227
|
+
|
228
|
+
with DatabaseConnection(db_path) as conn:
|
229
|
+
|
230
|
+
# Get index info
|
231
|
+
result = conn.execute(
|
232
|
+
"""
|
233
|
+
SELECT name, tbl_name, sql
|
234
|
+
FROM sqlite_master
|
235
|
+
WHERE type='index'
|
236
|
+
AND name=?
|
237
|
+
""",
|
238
|
+
[name]
|
239
|
+
)
|
240
|
+
|
241
|
+
row = result.fetchone()
|
242
|
+
if not row:
|
243
|
+
raise ValueError(f"Index '{name}' does not exist")
|
244
|
+
|
245
|
+
index_name, table_name, sql = row
|
246
|
+
|
247
|
+
# Parse unique from SQL
|
248
|
+
is_unique = "CREATE UNIQUE INDEX" in (sql or "").upper()
|
249
|
+
|
250
|
+
# Get indexed columns with more details
|
251
|
+
pragma_result = conn.execute(f"PRAGMA index_info({index_name})")
|
252
|
+
columns_info = []
|
253
|
+
for info in pragma_result.fetchall():
|
254
|
+
columns_info.append({
|
255
|
+
"position": info[0],
|
256
|
+
"column_id": info[1],
|
257
|
+
"column_name": info[2]
|
258
|
+
})
|
259
|
+
|
260
|
+
# Get index statistics
|
261
|
+
xinfo_result = conn.execute(f"PRAGMA index_xinfo({index_name})")
|
262
|
+
extended_info = xinfo_result.fetchall()
|
263
|
+
|
264
|
+
return {
|
265
|
+
"name": index_name,
|
266
|
+
"table": table_name,
|
267
|
+
"columns": [col["column_name"] for col in columns_info],
|
268
|
+
"columns_info": columns_info,
|
269
|
+
"unique": is_unique,
|
270
|
+
"sql": sql,
|
271
|
+
"partial": sql and "WHERE" in sql.upper() if sql else False
|
272
|
+
}
|
273
|
+
|
274
|
+
def _track_change(
|
275
|
+
self, change_type: ChangeType, entity_name: str, metadata: Dict[str, Any]
|
276
|
+
) -> None:
|
277
|
+
"""Track a change for this branch.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
change_type: Type of change
|
281
|
+
entity_name: Name of the entity being changed
|
282
|
+
metadata: Additional metadata about the change
|
283
|
+
"""
|
284
|
+
# Import here to avoid circular dependency
|
285
|
+
from cinchdb.managers.change_tracker import ChangeTracker
|
286
|
+
|
287
|
+
tracker = ChangeTracker(self.project_dir, self.database, self.branch)
|
288
|
+
|
289
|
+
change = Change(
|
290
|
+
id=str(uuid.uuid4()),
|
291
|
+
type=change_type,
|
292
|
+
entity_type="index",
|
293
|
+
entity_name=entity_name,
|
294
|
+
branch=self.branch,
|
295
|
+
metadata=metadata,
|
296
|
+
applied=True,
|
297
|
+
created_at=datetime.now(timezone.utc),
|
298
|
+
)
|
299
|
+
|
300
|
+
tracker.add_change(change)
|
@@ -1 +0,0 @@
|
|
1
|
-
"""CLI command modules."""
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|