cinchdb 0.1.6__py3-none-any.whl → 0.1.8__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/cli/commands/__init__.py +15 -0
- cinchdb/cli/commands/index.py +186 -0
- cinchdb/cli/main.py +2 -0
- cinchdb/core/database.py +67 -0
- cinchdb/managers/index.py +304 -0
- cinchdb/managers/table.py +21 -3
- cinchdb/managers/tenant.py +4 -0
- cinchdb/models/__init__.py +2 -1
- cinchdb/models/table.py +10 -0
- {cinchdb-0.1.6.dist-info → cinchdb-0.1.8.dist-info}/METADATA +1 -1
- {cinchdb-0.1.6.dist-info → cinchdb-0.1.8.dist-info}/RECORD +14 -12
- {cinchdb-0.1.6.dist-info → cinchdb-0.1.8.dist-info}/WHEEL +0 -0
- {cinchdb-0.1.6.dist-info → cinchdb-0.1.8.dist-info}/entry_points.txt +0 -0
- {cinchdb-0.1.6.dist-info → cinchdb-0.1.8.dist-info}/licenses/LICENSE +0 -0
cinchdb/cli/commands/__init__.py
CHANGED
@@ -1 +1,16 @@
|
|
1
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()
|
cinchdb/cli/main.py
CHANGED
@@ -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
|
|
cinchdb/core/database.py
CHANGED
@@ -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(
|
@@ -460,6 +477,56 @@ class CinchDB:
|
|
460
477
|
# Remote delete
|
461
478
|
self._make_request("DELETE", f"/tables/{table}/data/{id}")
|
462
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
|
+
# Convert parameters to Index model for validation
|
511
|
+
from cinchdb.models import Index
|
512
|
+
index = Index(columns=columns, name=name, unique=unique)
|
513
|
+
|
514
|
+
if self.is_local:
|
515
|
+
return self.indexes.create_index(table, index.columns, index.name, index.unique)
|
516
|
+
else:
|
517
|
+
# Remote index creation
|
518
|
+
result = self._make_request(
|
519
|
+
"POST",
|
520
|
+
"/indexes",
|
521
|
+
json={
|
522
|
+
"table": table,
|
523
|
+
"columns": index.columns,
|
524
|
+
"name": index.name,
|
525
|
+
"unique": index.unique,
|
526
|
+
},
|
527
|
+
)
|
528
|
+
return result.get("name")
|
529
|
+
|
463
530
|
def list_changes(self) -> List["Change"]:
|
464
531
|
"""List all changes for the current branch.
|
465
532
|
|
@@ -0,0 +1,304 @@
|
|
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
|
+
# Convert parameters to Index model for validation
|
56
|
+
from cinchdb.models import Index
|
57
|
+
index = Index(columns=columns, name=name, unique=unique)
|
58
|
+
|
59
|
+
if not index.columns:
|
60
|
+
raise ValueError("At least one column must be specified for the index")
|
61
|
+
|
62
|
+
# Generate index name if not provided
|
63
|
+
if not index.name:
|
64
|
+
column_str = "_".join(index.columns)
|
65
|
+
unique_prefix = "uniq_" if index.unique else "idx_"
|
66
|
+
index.name = f"{unique_prefix}{table}_{column_str}"
|
67
|
+
|
68
|
+
# Get connection to main tenant database (indexes are branch-level)
|
69
|
+
db_path = get_tenant_db_path(
|
70
|
+
self.project_dir, self.database, self.branch, "main"
|
71
|
+
)
|
72
|
+
|
73
|
+
with DatabaseConnection(db_path) as conn:
|
74
|
+
# Verify table exists
|
75
|
+
result = conn.execute(
|
76
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
77
|
+
[table]
|
78
|
+
)
|
79
|
+
if not result.fetchone():
|
80
|
+
raise ValueError(f"Table '{table}' does not exist")
|
81
|
+
|
82
|
+
# Verify columns exist
|
83
|
+
result = conn.execute(f"PRAGMA table_info({table})")
|
84
|
+
existing_columns = {row[1] for row in result.fetchall()}
|
85
|
+
|
86
|
+
invalid_columns = set(index.columns) - existing_columns
|
87
|
+
if invalid_columns:
|
88
|
+
raise ValueError(
|
89
|
+
f"Columns {invalid_columns} do not exist in table '{table}'"
|
90
|
+
)
|
91
|
+
|
92
|
+
# Build and execute CREATE INDEX statement
|
93
|
+
unique_clause = "UNIQUE " if index.unique else ""
|
94
|
+
if_not_exists_clause = "IF NOT EXISTS " if if_not_exists else ""
|
95
|
+
column_list = ", ".join(index.columns)
|
96
|
+
|
97
|
+
sql = f"CREATE {unique_clause}INDEX {if_not_exists_clause}{index.name} ON {table} ({column_list})"
|
98
|
+
|
99
|
+
try:
|
100
|
+
result = conn.execute(sql)
|
101
|
+
conn.commit()
|
102
|
+
except sqlite3.Error as e:
|
103
|
+
if "already exists" in str(e):
|
104
|
+
if not if_not_exists:
|
105
|
+
raise ValueError(f"Index '{index.name}' already exists")
|
106
|
+
else:
|
107
|
+
raise
|
108
|
+
|
109
|
+
# Track the change
|
110
|
+
self._track_change(
|
111
|
+
ChangeType.CREATE_INDEX,
|
112
|
+
index.name,
|
113
|
+
{"table": table, "columns": index.columns, "unique": index.unique}
|
114
|
+
)
|
115
|
+
|
116
|
+
return index.name
|
117
|
+
|
118
|
+
def drop_index(self, name: str, if_exists: bool = True) -> None:
|
119
|
+
"""Drop an index.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
name: Index name
|
123
|
+
if_exists: Whether to use IF EXISTS clause
|
124
|
+
|
125
|
+
Raises:
|
126
|
+
ValueError: If index doesn't exist and if_exists is False
|
127
|
+
"""
|
128
|
+
# Get connection to main tenant database (indexes are branch-level)
|
129
|
+
db_path = get_tenant_db_path(
|
130
|
+
self.project_dir, self.database, self.branch, "main"
|
131
|
+
)
|
132
|
+
|
133
|
+
with DatabaseConnection(db_path) as conn:
|
134
|
+
|
135
|
+
# Check if index exists
|
136
|
+
result = conn.execute(
|
137
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
138
|
+
[name]
|
139
|
+
)
|
140
|
+
exists = result.fetchone() is not None
|
141
|
+
|
142
|
+
if not exists and not if_exists:
|
143
|
+
raise ValueError(f"Index '{name}' does not exist")
|
144
|
+
|
145
|
+
if exists:
|
146
|
+
if_exists_clause = "IF EXISTS " if if_exists else ""
|
147
|
+
sql = f"DROP INDEX {if_exists_clause}{name}"
|
148
|
+
|
149
|
+
result = conn.execute(sql)
|
150
|
+
conn.commit()
|
151
|
+
|
152
|
+
# Track the change
|
153
|
+
self._track_change(ChangeType.DROP_INDEX, name, {})
|
154
|
+
|
155
|
+
def list_indexes(self, table: Optional[str] = None) -> List[Dict[str, Any]]:
|
156
|
+
"""List indexes for a table or all tables.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
table: Optional table name to filter indexes
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
List of index information dictionaries
|
163
|
+
"""
|
164
|
+
# Get connection to main tenant database (indexes are branch-level)
|
165
|
+
db_path = get_tenant_db_path(
|
166
|
+
self.project_dir, self.database, self.branch, "main"
|
167
|
+
)
|
168
|
+
|
169
|
+
indexes = []
|
170
|
+
|
171
|
+
with DatabaseConnection(db_path) as conn:
|
172
|
+
|
173
|
+
# Get all indexes (excluding SQLite internal indexes)
|
174
|
+
if table:
|
175
|
+
result = conn.execute(
|
176
|
+
"""
|
177
|
+
SELECT name, tbl_name, sql
|
178
|
+
FROM sqlite_master
|
179
|
+
WHERE type='index'
|
180
|
+
AND tbl_name=?
|
181
|
+
AND sql IS NOT NULL
|
182
|
+
""",
|
183
|
+
[table]
|
184
|
+
)
|
185
|
+
else:
|
186
|
+
result = conn.execute(
|
187
|
+
"""
|
188
|
+
SELECT name, tbl_name, sql
|
189
|
+
FROM sqlite_master
|
190
|
+
WHERE type='index'
|
191
|
+
AND sql IS NOT NULL
|
192
|
+
"""
|
193
|
+
)
|
194
|
+
|
195
|
+
for row in result.fetchall():
|
196
|
+
index_name, table_name, sql = row
|
197
|
+
|
198
|
+
# Parse unique from SQL
|
199
|
+
is_unique = "CREATE UNIQUE INDEX" in sql.upper()
|
200
|
+
|
201
|
+
# Get indexed columns
|
202
|
+
pragma_result = conn.execute(f"PRAGMA index_info({index_name})")
|
203
|
+
columns = [info[2] for info in pragma_result.fetchall()]
|
204
|
+
|
205
|
+
indexes.append({
|
206
|
+
"name": index_name,
|
207
|
+
"table": table_name,
|
208
|
+
"columns": columns,
|
209
|
+
"unique": is_unique,
|
210
|
+
"sql": sql
|
211
|
+
})
|
212
|
+
|
213
|
+
return indexes
|
214
|
+
|
215
|
+
def get_index_info(self, name: str) -> Dict[str, Any]:
|
216
|
+
"""Get detailed information about a specific index.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
name: Index name
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Dictionary with index information
|
223
|
+
|
224
|
+
Raises:
|
225
|
+
ValueError: If index doesn't exist
|
226
|
+
"""
|
227
|
+
# Get connection to main tenant database (indexes are branch-level)
|
228
|
+
db_path = get_tenant_db_path(
|
229
|
+
self.project_dir, self.database, self.branch, "main"
|
230
|
+
)
|
231
|
+
|
232
|
+
with DatabaseConnection(db_path) as conn:
|
233
|
+
|
234
|
+
# Get index info
|
235
|
+
result = conn.execute(
|
236
|
+
"""
|
237
|
+
SELECT name, tbl_name, sql
|
238
|
+
FROM sqlite_master
|
239
|
+
WHERE type='index'
|
240
|
+
AND name=?
|
241
|
+
""",
|
242
|
+
[name]
|
243
|
+
)
|
244
|
+
|
245
|
+
row = result.fetchone()
|
246
|
+
if not row:
|
247
|
+
raise ValueError(f"Index '{name}' does not exist")
|
248
|
+
|
249
|
+
index_name, table_name, sql = row
|
250
|
+
|
251
|
+
# Parse unique from SQL
|
252
|
+
is_unique = "CREATE UNIQUE INDEX" in (sql or "").upper()
|
253
|
+
|
254
|
+
# Get indexed columns with more details
|
255
|
+
pragma_result = conn.execute(f"PRAGMA index_info({index_name})")
|
256
|
+
columns_info = []
|
257
|
+
for info in pragma_result.fetchall():
|
258
|
+
columns_info.append({
|
259
|
+
"position": info[0],
|
260
|
+
"column_id": info[1],
|
261
|
+
"column_name": info[2]
|
262
|
+
})
|
263
|
+
|
264
|
+
# Get index statistics
|
265
|
+
xinfo_result = conn.execute(f"PRAGMA index_xinfo({index_name})")
|
266
|
+
extended_info = xinfo_result.fetchall()
|
267
|
+
|
268
|
+
return {
|
269
|
+
"name": index_name,
|
270
|
+
"table": table_name,
|
271
|
+
"columns": [col["column_name"] for col in columns_info],
|
272
|
+
"columns_info": columns_info,
|
273
|
+
"unique": is_unique,
|
274
|
+
"sql": sql,
|
275
|
+
"partial": sql and "WHERE" in sql.upper() if sql else False
|
276
|
+
}
|
277
|
+
|
278
|
+
def _track_change(
|
279
|
+
self, change_type: ChangeType, entity_name: str, metadata: Dict[str, Any]
|
280
|
+
) -> None:
|
281
|
+
"""Track a change for this branch.
|
282
|
+
|
283
|
+
Args:
|
284
|
+
change_type: Type of change
|
285
|
+
entity_name: Name of the entity being changed
|
286
|
+
metadata: Additional metadata about the change
|
287
|
+
"""
|
288
|
+
# Import here to avoid circular dependency
|
289
|
+
from cinchdb.managers.change_tracker import ChangeTracker
|
290
|
+
|
291
|
+
tracker = ChangeTracker(self.project_dir, self.database, self.branch)
|
292
|
+
|
293
|
+
change = Change(
|
294
|
+
id=str(uuid.uuid4()),
|
295
|
+
type=change_type,
|
296
|
+
entity_type="index",
|
297
|
+
entity_name=entity_name,
|
298
|
+
branch=self.branch,
|
299
|
+
metadata=metadata,
|
300
|
+
applied=True,
|
301
|
+
created_at=datetime.now(timezone.utc),
|
302
|
+
)
|
303
|
+
|
304
|
+
tracker.add_change(change)
|
cinchdb/managers/table.py
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
"""Table management for CinchDB."""
|
2
2
|
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import List
|
4
|
+
from typing import List, Optional, TYPE_CHECKING
|
5
5
|
|
6
6
|
from cinchdb.models import Table, Column, Change, ChangeType
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from cinchdb.models import Index
|
7
10
|
from cinchdb.core.connection import DatabaseConnection
|
8
11
|
from cinchdb.core.path_utils import get_tenant_db_path
|
9
12
|
from cinchdb.core.maintenance import check_maintenance_mode
|
@@ -61,12 +64,13 @@ class TableManager:
|
|
61
64
|
|
62
65
|
return tables
|
63
66
|
|
64
|
-
def create_table(self, table_name: str, columns: List[Column]) -> Table:
|
65
|
-
"""Create a new table with optional foreign key constraints.
|
67
|
+
def create_table(self, table_name: str, columns: List[Column], indexes: Optional[List["Index"]] = None) -> Table:
|
68
|
+
"""Create a new table with optional foreign key constraints and indexes.
|
66
69
|
|
67
70
|
Args:
|
68
71
|
table_name: Name of the table
|
69
72
|
columns: List of Column objects defining the schema
|
73
|
+
indexes: Optional list of Index objects to create on the table
|
70
74
|
|
71
75
|
Returns:
|
72
76
|
Created Table object
|
@@ -168,6 +172,20 @@ class TableManager:
|
|
168
172
|
applier = ChangeApplier(self.project_root, self.database, self.branch)
|
169
173
|
applier.apply_change(change.id)
|
170
174
|
|
175
|
+
# Create indexes if specified
|
176
|
+
if indexes:
|
177
|
+
from cinchdb.managers.index import IndexManager
|
178
|
+
index_manager = IndexManager(self.project_root, self.database, self.branch)
|
179
|
+
|
180
|
+
for index in indexes:
|
181
|
+
index_manager.create_index(
|
182
|
+
table=table_name,
|
183
|
+
columns=index.columns,
|
184
|
+
name=index.name,
|
185
|
+
unique=index.unique,
|
186
|
+
if_not_exists=True
|
187
|
+
)
|
188
|
+
|
171
189
|
# Return the created table
|
172
190
|
return Table(
|
173
191
|
name=table_name,
|
cinchdb/managers/tenant.py
CHANGED
@@ -104,6 +104,10 @@ class TenantManager:
|
|
104
104
|
conn.execute(f"DELETE FROM {table}")
|
105
105
|
|
106
106
|
conn.commit()
|
107
|
+
|
108
|
+
# Open a new connection to vacuum the database
|
109
|
+
with DatabaseConnection(new_db_path) as conn:
|
110
|
+
conn.execute("VACUUM")
|
107
111
|
|
108
112
|
return Tenant(
|
109
113
|
name=tenant_name,
|
cinchdb/models/__init__.py
CHANGED
@@ -5,7 +5,7 @@ from .project import Project
|
|
5
5
|
from .database import Database
|
6
6
|
from .branch import Branch
|
7
7
|
from .tenant import Tenant
|
8
|
-
from .table import Table, Column, ColumnType, ForeignKeyRef, ForeignKeyAction
|
8
|
+
from .table import Table, Column, ColumnType, ForeignKeyRef, ForeignKeyAction, Index
|
9
9
|
from .view import View
|
10
10
|
from .change import Change, ChangeType
|
11
11
|
|
@@ -21,6 +21,7 @@ __all__ = [
|
|
21
21
|
"ColumnType",
|
22
22
|
"ForeignKeyRef",
|
23
23
|
"ForeignKeyAction",
|
24
|
+
"Index",
|
24
25
|
"View",
|
25
26
|
"Change",
|
26
27
|
"ChangeType",
|
cinchdb/models/table.py
CHANGED
@@ -27,6 +27,16 @@ class ForeignKeyRef(BaseModel):
|
|
27
27
|
)
|
28
28
|
|
29
29
|
|
30
|
+
class Index(BaseModel):
|
31
|
+
"""Index specification for table columns."""
|
32
|
+
|
33
|
+
model_config = ConfigDict(extra="forbid")
|
34
|
+
|
35
|
+
columns: List[str] = Field(description="Column names to index")
|
36
|
+
name: Optional[str] = Field(default=None, description="Index name")
|
37
|
+
unique: bool = Field(default=False, description="Create unique index")
|
38
|
+
|
39
|
+
|
30
40
|
class Column(BaseModel):
|
31
41
|
"""Represents a column in a table."""
|
32
42
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: cinchdb
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.8
|
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
|
@@ -2,13 +2,14 @@ cinchdb/__init__.py,sha256=NZdSzfhRguSBTjJ2dcESOQYy53OZEuBndlB7U08GMY0,179
|
|
2
2
|
cinchdb/__main__.py,sha256=OpkDqn9zkTZhhYgvv_grswWLAHKbmxs4M-8C6Z5HfWY,85
|
3
3
|
cinchdb/config.py,sha256=gocjMnYKLWhgvnteo6zprgwtK6Oevoxq547J_v-C9Ns,5265
|
4
4
|
cinchdb/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
5
|
-
cinchdb/cli/main.py,sha256=
|
5
|
+
cinchdb/cli/main.py,sha256=tGX8Z78Oy4rukXth56Xp_pm52rijIDujyMdhSEhFWPw,4790
|
6
6
|
cinchdb/cli/utils.py,sha256=NREFxN9k53FnPbDoPt4SXmdZzlzw9zUMv5ICQwTT8gk,5679
|
7
|
-
cinchdb/cli/commands/__init__.py,sha256=
|
7
|
+
cinchdb/cli/commands/__init__.py,sha256=IRUIPHgdpF4hBDbDy0SgaWn39o5GNUjH54_C42sjlQg,273
|
8
8
|
cinchdb/cli/commands/branch.py,sha256=Nz8YQYJ7lizSXEAv0usTx85TDOC-N5Ul9KIxN8JQtKc,17973
|
9
9
|
cinchdb/cli/commands/codegen.py,sha256=WsRWmXNTDuaLPyECW5psXM9zOQnKHpUiv8BJnBAjMII,6189
|
10
10
|
cinchdb/cli/commands/column.py,sha256=ISHRmcoLf1fAbPqC2MaAYH7Fc6xZWtzCMSRh7_9o-lY,11757
|
11
11
|
cinchdb/cli/commands/database.py,sha256=-UCOnn3VatdNog-fX5pguJ2GKdSSXQN99-LSVwkvinY,6857
|
12
|
+
cinchdb/cli/commands/index.py,sha256=P0sM9lu1rVliacYl49LPqtxNYdrwzKzpVKW10jC-5i4,6096
|
12
13
|
cinchdb/cli/commands/query.py,sha256=XW_YL6M5IYHHHMpVB5p-M01kawFxwDOK5B5hGIy_BA8,5044
|
13
14
|
cinchdb/cli/commands/remote.py,sha256=i07hfiAxgrROB9lVJVaKK_nWxT1SGiSbtFb4jvEwxEo,4445
|
14
15
|
cinchdb/cli/commands/table.py,sha256=NxfOTCd9beaujffiAPiW0Vko0--HS1JVeCwBMp_khx4,10518
|
@@ -18,7 +19,7 @@ cinchdb/cli/handlers/__init__.py,sha256=f2f-Cc96rSBLbVsiIbf-b4pZCKZoHfmhNEvnZ0Ou
|
|
18
19
|
cinchdb/cli/handlers/codegen_handler.py,sha256=i5we_AbiUW3zfO6pIKWxvtO8OvOqz3H__4xPmTLEuQM,6524
|
19
20
|
cinchdb/core/__init__.py,sha256=iNlT0iO9cM0HLoYwzBavUBoXRh1Tcnz1l_vfbwVxK_Q,246
|
20
21
|
cinchdb/core/connection.py,sha256=SlKyEfIpeaDws8M6SfEbvCEVnt26zBY1RYwHtTXj0kY,5110
|
21
|
-
cinchdb/core/database.py,sha256=
|
22
|
+
cinchdb/core/database.py,sha256=yeBxzxgKz20Dtoilzj609GHrD3UL2bWG4ArWrRM2VnA,22090
|
22
23
|
cinchdb/core/initializer.py,sha256=CjnJSMuR1NrHobyFfwL44tUeH8VE62q02bijEtVH3p4,6922
|
23
24
|
cinchdb/core/maintenance.py,sha256=PAgrSL7Cj9p3rKHV0h_L7gupN6nLD0-5eQpJZNiqyEs,2097
|
24
25
|
cinchdb/core/path_utils.py,sha256=J2UEu1X_NFOqDamcsrPrC7ZitGTg9Y-HFjmx4sHf5j8,3806
|
@@ -30,25 +31,26 @@ cinchdb/managers/change_tracker.py,sha256=U93BPnuGv8xSaO5qr_y5Q8ppKrVXygozdp5zUv
|
|
30
31
|
cinchdb/managers/codegen.py,sha256=1CfIwjgHnNDdjrq4SzQ9VE7DFgnWfk7RtpupBFUTqxk,21804
|
31
32
|
cinchdb/managers/column.py,sha256=YhYq-hnH0o2BqZkyihnsY5KIWEztzs-_iLJNZMdVUkk,20807
|
32
33
|
cinchdb/managers/data.py,sha256=zS1HkMGf436m6f8VdFAqQbQFgo4sL5yKJRcRf4A6lIc,16253
|
34
|
+
cinchdb/managers/index.py,sha256=n9bCXggZP6muJQZXCpTT46JvuvcbbnYgeV3j6iXtTVM,10371
|
33
35
|
cinchdb/managers/merge_manager.py,sha256=R8S2hLkLJg4hLDpeJTzjVkduZgqPOjXtYgOSJhTXXrE,15690
|
34
36
|
cinchdb/managers/query.py,sha256=pBlbqoovnFsZ36pB7nv8NtzcTFwtT26hp8IlwjIx29Q,7301
|
35
|
-
cinchdb/managers/table.py,sha256=
|
36
|
-
cinchdb/managers/tenant.py,sha256=
|
37
|
+
cinchdb/managers/table.py,sha256=GltELZ465M8JYwZB5xoMDOvyhRYm-HflPJsQQTStD2c,13837
|
38
|
+
cinchdb/managers/tenant.py,sha256=Wx32u71ejZEKStaPRTPw_TqCoE8XzAqFfCRNvmBfNlE,8879
|
37
39
|
cinchdb/managers/view.py,sha256=v9gYtRufZyxywPKLGvIjvlUXcxYh9CLRArefu9QX6zk,7809
|
38
|
-
cinchdb/models/__init__.py,sha256=
|
40
|
+
cinchdb/models/__init__.py,sha256=cZ-ailJ6qu44Iap5Rq555iB-_w9ufXVDBH3rDH-ojk0,622
|
39
41
|
cinchdb/models/base.py,sha256=7j4rlFTP5K9ZuF8vxwC7lMFEaL7O90NJ47Ig5i7ubcw,1320
|
40
42
|
cinchdb/models/branch.py,sha256=gRgLpRFkMC3fxf9ZigVOkS6wdkBERWqlLk0_gOYjqNk,1180
|
41
43
|
cinchdb/models/change.py,sha256=YpBWdI6yMT3uucd8duET9s75xr5JUWJqurkkyTlXPlk,1449
|
42
44
|
cinchdb/models/database.py,sha256=QrWd_SkE1G8TMWflO4sXRUbSdbqcrfGOt2e-PS7OW7A,971
|
43
45
|
cinchdb/models/project.py,sha256=6GMXUZUsEIebqQJgRXIthWzpWKuNNmJ3drgI1vFDrMo,644
|
44
|
-
cinchdb/models/table.py,sha256=
|
46
|
+
cinchdb/models/table.py,sha256=hamYjYUunNnVBMcE5NNyubS5Y8_wTJ8_aMnA5r8mpkE,3097
|
45
47
|
cinchdb/models/tenant.py,sha256=UKYTKM4mQH3IqEjI_tOU5CszwBWH4cXa3lI0mpMFF_4,967
|
46
48
|
cinchdb/models/view.py,sha256=q6j-jYzFJuhRJO87rKt6Uv8hOizHQx8xwoPKoH6XnNY,530
|
47
49
|
cinchdb/utils/__init__.py,sha256=yQQhEjndDiB2SUJybUmp9dvEOQKiR-GySe-WiCius5E,490
|
48
50
|
cinchdb/utils/name_validator.py,sha256=dyGX5bjlTFRA9EGrWRQKp6kR__HSV04hLV5VueJs4IQ,4027
|
49
51
|
cinchdb/utils/sql_validator.py,sha256=aWOGlPX0gBkuR6R1EBP2stbP4PHZuI6FUBi2Ljx7JUI,5815
|
50
|
-
cinchdb-0.1.
|
51
|
-
cinchdb-0.1.
|
52
|
-
cinchdb-0.1.
|
53
|
-
cinchdb-0.1.
|
54
|
-
cinchdb-0.1.
|
52
|
+
cinchdb-0.1.8.dist-info/METADATA,sha256=Lx66m_6wkzVWt_N2rtcO5xVBVL5XHNxlfaOI-5-DN30,6334
|
53
|
+
cinchdb-0.1.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
54
|
+
cinchdb-0.1.8.dist-info/entry_points.txt,sha256=VBOIzvnGbkKudMCCmNORS3885QSyjZUVKJQ-Syqa62w,47
|
55
|
+
cinchdb-0.1.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
56
|
+
cinchdb-0.1.8.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|