cinchdb 0.1.3__py3-none-any.whl → 0.1.5__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/config.py CHANGED
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, ConfigDict
9
9
 
10
10
  class RemoteConfig(BaseModel):
11
11
  """Configuration for a remote CinchDB instance."""
12
-
12
+
13
13
  url: str = Field(description="Base URL of the remote CinchDB API")
14
14
  key: str = Field(description="API key for authentication")
15
15
 
@@ -50,7 +50,7 @@ class Config:
50
50
  env_dir = os.environ.get("CINCHDB_PROJECT_DIR")
51
51
  if env_dir:
52
52
  project_dir = Path(env_dir)
53
-
53
+
54
54
  self.project_dir = Path(project_dir) if project_dir else Path.cwd()
55
55
  self.config_dir = self.project_dir / ".cinchdb"
56
56
  self.config_path = self.config_dir / "config.toml"
@@ -86,24 +86,24 @@ class Config:
86
86
  # Override database and branch
87
87
  if env_db := os.environ.get("CINCHDB_DATABASE"):
88
88
  data["active_database"] = env_db
89
-
89
+
90
90
  if env_branch := os.environ.get("CINCHDB_BRANCH"):
91
91
  data["active_branch"] = env_branch
92
-
92
+
93
93
  # Override or create remote configuration
94
94
  env_url = os.environ.get("CINCHDB_REMOTE_URL")
95
95
  env_key = os.environ.get("CINCHDB_API_KEY")
96
-
96
+
97
97
  if env_url and env_key:
98
98
  # Create or update "env" remote
99
99
  if "remotes" not in data:
100
100
  data["remotes"] = {}
101
-
101
+
102
102
  data["remotes"]["env"] = {
103
103
  "url": env_url.rstrip("/"), # Remove trailing slash
104
- "key": env_key
104
+ "key": env_key,
105
105
  }
106
-
106
+
107
107
  # Make it active if no other remote is set
108
108
  if not data.get("active_remote"):
109
109
  data["active_remote"] = "env"
@@ -130,48 +130,21 @@ class Config:
130
130
  for alias, remote in config_dict["remotes"].items():
131
131
  if isinstance(remote, dict):
132
132
  config_dict["remotes"][alias] = remote
133
-
133
+
134
134
  with open(self.config_path, "w") as f:
135
135
  toml.dump(config_dict, f)
136
136
 
137
137
  def init_project(self) -> ProjectConfig:
138
- """Initialize a new CinchDB project with default configuration."""
139
- if self.exists:
140
- raise FileExistsError(f"Project already exists at {self.config_dir}")
138
+ """Initialize a new CinchDB project with default configuration.
141
139
 
142
- # Create default config
143
- config = ProjectConfig()
144
- self.save(config)
140
+ This method now delegates to the ProjectInitializer for the actual
141
+ initialization logic.
142
+ """
143
+ from cinchdb.core.initializer import ProjectInitializer
145
144
 
146
- # Create default database structure
147
- self._create_default_structure()
145
+ initializer = ProjectInitializer(self.project_dir)
146
+ config = initializer.init_project()
148
147
 
148
+ # Load the config into this instance
149
+ self._config = config
149
150
  return config
150
-
151
- def _create_default_structure(self) -> None:
152
- """Create default project structure."""
153
- # Create main database with main branch
154
- db_path = self.config_dir / "databases" / "main" / "branches" / "main"
155
- db_path.mkdir(parents=True, exist_ok=True)
156
-
157
- # Create metadata files
158
- from datetime import datetime, timezone
159
-
160
- metadata = {"created_at": datetime.now(timezone.utc).isoformat()}
161
-
162
- import json
163
-
164
- with open(db_path / "metadata.json", "w") as f:
165
- json.dump(metadata, f, indent=2)
166
-
167
- # Create empty changes file
168
- with open(db_path / "changes.json", "w") as f:
169
- json.dump([], f, indent=2)
170
-
171
- # Create main tenant directory and database
172
- tenant_dir = db_path / "tenants"
173
- tenant_dir.mkdir(exist_ok=True)
174
-
175
- # Create main tenant database file
176
- main_db = tenant_dir / "main.db"
177
- main_db.touch()
cinchdb/core/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """Core CinchDB functionality."""
2
2
 
3
3
  from cinchdb.core.database import CinchDB, connect, connect_api
4
+ from cinchdb.core.initializer import init_project, init_database
4
5
 
5
- __all__ = ["CinchDB", "connect", "connect_api"]
6
+ __all__ = ["CinchDB", "connect", "connect_api", "init_project", "init_database"]
cinchdb/core/database.py CHANGED
@@ -3,10 +3,9 @@
3
3
  from pathlib import Path
4
4
  from typing import List, Dict, Any, Optional, TYPE_CHECKING
5
5
 
6
- from cinchdb.config import Config
7
6
  from cinchdb.models import Column, Change
8
7
  from cinchdb.core.path_utils import get_project_root
9
- from cinchdb.utils import validate_query_safe, SQLValidationError
8
+ from cinchdb.utils import validate_query_safe
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from cinchdb.managers.table import TableManager
@@ -307,7 +306,10 @@ class CinchDB:
307
306
  # Convenience methods for common operations
308
307
 
309
308
  def query(
310
- self, sql: str, params: Optional[List[Any]] = None, skip_validation: bool = False
309
+ self,
310
+ sql: str,
311
+ params: Optional[List[Any]] = None,
312
+ skip_validation: bool = False,
311
313
  ) -> List[Dict[str, Any]]:
312
314
  """Execute a SQL query.
313
315
 
@@ -318,14 +320,14 @@ class CinchDB:
318
320
 
319
321
  Returns:
320
322
  List of result rows as dictionaries
321
-
323
+
322
324
  Raises:
323
325
  SQLValidationError: If the query contains restricted operations
324
326
  """
325
327
  # Validate query unless explicitly skipped
326
328
  if not skip_validation:
327
329
  validate_query_safe(sql)
328
-
330
+
329
331
  if self.is_local:
330
332
  if self._query_manager is None:
331
333
  from cinchdb.managers.query import QueryManager
@@ -369,10 +371,24 @@ class CinchDB:
369
371
  data: Record data as dictionary
370
372
 
371
373
  Returns:
372
- Inserted record with generated fields
374
+ Inserted record with generated fields (id, created_at, updated_at)
375
+
376
+ Examples:
377
+ # Simple insert
378
+ db.insert("users", {"name": "John", "email": "john@example.com"})
379
+
380
+ # Insert with custom ID
381
+ db.insert("products", {"id": "prod-123", "name": "Widget", "price": 9.99})
373
382
  """
374
383
  if self.is_local:
375
- return self.data.create(table, data)
384
+ # Initialize data manager if needed
385
+ if self._data_manager is None:
386
+ from cinchdb.managers.data import DataManager
387
+ self._data_manager = DataManager(
388
+ self.project_dir, self.database, self.branch, self.tenant
389
+ )
390
+ # Use the new create_from_dict method
391
+ return self._data_manager.create_from_dict(table, data)
376
392
  else:
377
393
  # Remote insert - use new data CRUD endpoint
378
394
  result = self._make_request(
@@ -427,6 +443,7 @@ class CinchDB:
427
443
  """
428
444
  if self.is_local:
429
445
  from cinchdb.managers.change_tracker import ChangeTracker
446
+
430
447
  tracker = ChangeTracker(self.project_dir, self.database, self.branch)
431
448
  return tracker.get_changes()
432
449
  else:
@@ -435,6 +452,7 @@ class CinchDB:
435
452
  # Convert API response to Change objects
436
453
  from cinchdb.models import Change
437
454
  from datetime import datetime
455
+
438
456
  changes = []
439
457
  for data in result.get("changes", []):
440
458
  # Convert string dates back to datetime if present
@@ -0,0 +1,214 @@
1
+ """Project initialization for CinchDB."""
2
+
3
+ import json
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from cinchdb.core.connection import DatabaseConnection
9
+ from cinchdb.config import ProjectConfig
10
+
11
+
12
+ class ProjectInitializer:
13
+ """Handles initialization of CinchDB projects."""
14
+
15
+ def __init__(self, project_dir: Optional[Path] = None):
16
+ """Initialize the project initializer.
17
+
18
+ Args:
19
+ project_dir: Path to project directory. If None, uses current directory.
20
+ """
21
+ self.project_dir = Path(project_dir) if project_dir else Path.cwd()
22
+ self.config_dir = self.project_dir / ".cinchdb"
23
+ self.config_path = self.config_dir / "config.toml"
24
+
25
+ def init_project(
26
+ self, database_name: str = "main", branch_name: str = "main"
27
+ ) -> ProjectConfig:
28
+ """Initialize a new CinchDB project with default configuration.
29
+
30
+ Args:
31
+ database_name: Name for the initial database (default: "main")
32
+ branch_name: Name for the initial branch (default: "main")
33
+
34
+ Returns:
35
+ The created ProjectConfig
36
+
37
+ Raises:
38
+ FileExistsError: If project already exists at the location
39
+ """
40
+ if self.config_path.exists():
41
+ raise FileExistsError(f"Project already exists at {self.config_dir}")
42
+
43
+ # Create config directory
44
+ self.config_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ # Create default config
47
+ config = ProjectConfig(active_database=database_name, active_branch=branch_name)
48
+
49
+ # Save config
50
+ self._save_config(config)
51
+
52
+ # Create default database structure
53
+ self._create_database_structure(database_name, branch_name)
54
+
55
+ return config
56
+
57
+ def init_database(
58
+ self,
59
+ database_name: str,
60
+ branch_name: str = "main",
61
+ description: Optional[str] = None,
62
+ ) -> None:
63
+ """Initialize a new database within an existing project.
64
+
65
+ Args:
66
+ database_name: Name for the database
67
+ branch_name: Initial branch name (default: "main")
68
+ description: Optional description for the database
69
+
70
+ Raises:
71
+ FileNotFoundError: If project doesn't exist
72
+ FileExistsError: If database already exists
73
+ """
74
+ if not self.config_path.exists():
75
+ raise FileNotFoundError(f"No CinchDB project found at {self.config_dir}")
76
+
77
+ db_path = self.config_dir / "databases" / database_name
78
+ if db_path.exists():
79
+ raise FileExistsError(f"Database '{database_name}' already exists")
80
+
81
+ # Create database structure
82
+ self._create_database_structure(database_name, branch_name, description)
83
+
84
+ def _create_database_structure(
85
+ self,
86
+ database_name: str,
87
+ branch_name: str = "main",
88
+ description: Optional[str] = None,
89
+ ) -> None:
90
+ """Create the directory structure for a database.
91
+
92
+ Args:
93
+ database_name: Name of the database
94
+ branch_name: Name of the initial branch
95
+ description: Optional description
96
+ """
97
+ # Create database branch path
98
+ branch_path = (
99
+ self.config_dir / "databases" / database_name / "branches" / branch_name
100
+ )
101
+ branch_path.mkdir(parents=True, exist_ok=True)
102
+
103
+ # Create metadata file
104
+ metadata = {
105
+ "created_at": datetime.now(timezone.utc).isoformat(),
106
+ "name": branch_name,
107
+ "parent": None,
108
+ "description": description,
109
+ }
110
+
111
+ with open(branch_path / "metadata.json", "w") as f:
112
+ json.dump(metadata, f, indent=2)
113
+
114
+ # Create empty changes file
115
+ with open(branch_path / "changes.json", "w") as f:
116
+ json.dump([], f, indent=2)
117
+
118
+ # Create tenants directory
119
+ tenant_dir = branch_path / "tenants"
120
+ tenant_dir.mkdir(exist_ok=True)
121
+
122
+ # Create and initialize main tenant database
123
+ self._init_tenant_database(tenant_dir / "main.db")
124
+
125
+ def _init_tenant_database(self, db_path: Path) -> None:
126
+ """Initialize a tenant database with proper PRAGMAs.
127
+
128
+ Args:
129
+ db_path: Path to the database file
130
+ """
131
+ # Create the database file
132
+ db_path.touch()
133
+
134
+ # Initialize with proper PRAGMAs
135
+ with DatabaseConnection(db_path):
136
+ # The connection automatically sets up PRAGMAs in _connect():
137
+ # - journal_mode = WAL
138
+ # - synchronous = NORMAL
139
+ # - wal_autocheckpoint = 0
140
+ # - foreign_keys = ON
141
+ pass
142
+
143
+ def _save_config(self, config: ProjectConfig) -> None:
144
+ """Save configuration to disk.
145
+
146
+ Args:
147
+ config: Configuration to save
148
+ """
149
+ import toml
150
+
151
+ # Ensure config directory exists
152
+ self.config_dir.mkdir(parents=True, exist_ok=True)
153
+
154
+ # Convert to dict for TOML serialization
155
+ config_dict = config.model_dump()
156
+
157
+ # Convert RemoteConfig objects to dicts if present
158
+ if "remotes" in config_dict:
159
+ for alias, remote in config_dict["remotes"].items():
160
+ if isinstance(remote, dict):
161
+ config_dict["remotes"][alias] = remote
162
+
163
+ with open(self.config_path, "w") as f:
164
+ toml.dump(config_dict, f)
165
+
166
+
167
+ def init_project(
168
+ project_dir: Optional[Path] = None,
169
+ database_name: str = "main",
170
+ branch_name: str = "main",
171
+ ) -> ProjectConfig:
172
+ """Initialize a new CinchDB project.
173
+
174
+ This is a convenience function that creates a ProjectInitializer
175
+ and initializes a project.
176
+
177
+ Args:
178
+ project_dir: Directory to initialize in (default: current directory)
179
+ database_name: Name for initial database (default: "main")
180
+ branch_name: Name for initial branch (default: "main")
181
+
182
+ Returns:
183
+ The created ProjectConfig
184
+
185
+ Raises:
186
+ FileExistsError: If project already exists
187
+ """
188
+ initializer = ProjectInitializer(project_dir)
189
+ return initializer.init_project(database_name, branch_name)
190
+
191
+
192
+ def init_database(
193
+ project_dir: Optional[Path] = None,
194
+ database_name: str = "main",
195
+ branch_name: str = "main",
196
+ description: Optional[str] = None,
197
+ ) -> None:
198
+ """Initialize a new database within an existing project.
199
+
200
+ This is a convenience function that creates a ProjectInitializer
201
+ and initializes a database.
202
+
203
+ Args:
204
+ project_dir: Project directory (default: current directory)
205
+ database_name: Name for the database
206
+ branch_name: Initial branch name (default: "main")
207
+ description: Optional description
208
+
209
+ Raises:
210
+ FileNotFoundError: If project doesn't exist
211
+ FileExistsError: If database already exists
212
+ """
213
+ initializer = ProjectInitializer(project_dir)
214
+ initializer.init_database(database_name, branch_name, description)
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from typing import List, Dict, Any
7
7
  from datetime import datetime, timezone
8
8
 
9
- from cinchdb.config import Config
10
9
  from cinchdb.models import Branch
11
10
  from cinchdb.core.path_utils import (
12
11
  get_database_path,
@@ -68,7 +67,7 @@ class BranchManager:
68
67
  """
69
68
  # Validate new branch name
70
69
  validate_name(new_branch_name, "branch")
71
-
70
+
72
71
  # Validate source branch exists
73
72
  if source_branch not in list_branches(self.project_root, self.database):
74
73
  raise ValueError(f"Source branch '{source_branch}' does not exist")
@@ -122,7 +121,6 @@ class BranchManager:
122
121
  branch_path = get_branch_path(self.project_root, self.database, branch_name)
123
122
  shutil.rmtree(branch_path)
124
123
 
125
-
126
124
  def get_branch_metadata(self, branch_name: str) -> Dict[str, Any]:
127
125
  """Get metadata for a branch.
128
126
 
@@ -439,16 +439,20 @@ class ColumnManager:
439
439
  conn.commit()
440
440
 
441
441
  def alter_column_nullable(
442
- self, table_name: str, column_name: str, nullable: bool, fill_value: Optional[Any] = None
442
+ self,
443
+ table_name: str,
444
+ column_name: str,
445
+ nullable: bool,
446
+ fill_value: Optional[Any] = None,
443
447
  ) -> None:
444
448
  """Change the nullable constraint on a column.
445
-
449
+
446
450
  Args:
447
451
  table_name: Name of the table
448
452
  column_name: Name of the column to modify
449
453
  nullable: Whether the column should allow NULL values
450
454
  fill_value: Value to use for existing NULL values when making column NOT NULL
451
-
455
+
452
456
  Raises:
453
457
  ValueError: If table/column doesn't exist or column is protected
454
458
  ValueError: If making NOT NULL and column has NULL values without fill_value
@@ -469,13 +473,13 @@ class ColumnManager:
469
473
  existing_columns = self.list_columns(table_name)
470
474
  column_found = False
471
475
  old_column = None
472
-
476
+
473
477
  for col in existing_columns:
474
478
  if col.name == column_name:
475
479
  column_found = True
476
480
  old_column = col
477
481
  break
478
-
482
+
479
483
  if not column_found:
480
484
  raise ValueError(
481
485
  f"Column '{column_name}' does not exist in table '{table_name}'"
@@ -494,7 +498,7 @@ class ColumnManager:
494
498
  f"SELECT COUNT(*) FROM {table_name} WHERE {column_name} IS NULL"
495
499
  )
496
500
  null_count = cursor.fetchone()[0]
497
-
501
+
498
502
  if null_count > 0 and fill_value is None:
499
503
  raise ValueError(
500
504
  f"Column '{column_name}' has {null_count} NULL values. "
@@ -503,7 +507,7 @@ class ColumnManager:
503
507
 
504
508
  # Build SQL statements for table recreation
505
509
  temp_table = f"{table_name}_temp"
506
-
510
+
507
511
  # Create new table with modified column
508
512
  col_defs = []
509
513
  for col in existing_columns:
@@ -527,7 +531,7 @@ class ColumnManager:
527
531
  # Column names for copying
528
532
  col_names = [col.name for col in existing_columns]
529
533
  col_list = ", ".join(col_names)
530
-
534
+
531
535
  # Build copy SQL with COALESCE if needed
532
536
  if not nullable and fill_value is not None:
533
537
  # Build select list with COALESCE for the target column
@@ -545,7 +549,7 @@ class ColumnManager:
545
549
  copy_sql = f"INSERT INTO {temp_table} ({col_list}) SELECT {select_list} FROM {table_name}"
546
550
  else:
547
551
  copy_sql = f"INSERT INTO {temp_table} ({col_list}) SELECT {col_list} FROM {table_name}"
548
-
552
+
549
553
  drop_sql = f"DROP TABLE {table_name}"
550
554
  rename_sql = f"ALTER TABLE {temp_table} RENAME TO {table_name}"
551
555
 
cinchdb/managers/data.py CHANGED
@@ -93,14 +93,15 @@ class DataManager:
93
93
  results = self.select(model_class, limit=1, id=record_id)
94
94
  return results[0] if results else None
95
95
 
96
- def create(self, instance: T) -> T:
97
- """Create a new record.
96
+ def create_from_dict(self, table_name: str, data: Dict[str, Any]) -> Dict[str, Any]:
97
+ """Create a new record from a dictionary.
98
98
 
99
99
  Args:
100
- instance: Model instance to create
100
+ table_name: Name of the table to insert into
101
+ data: Dictionary containing the record data
101
102
 
102
103
  Returns:
103
- Created model instance with populated ID and timestamps
104
+ Dictionary with created record including generated ID and timestamps
104
105
 
105
106
  Raises:
106
107
  ValueError: If record with same ID already exists
@@ -109,22 +110,20 @@ class DataManager:
109
110
  # Check maintenance mode
110
111
  check_maintenance_mode(self.project_root, self.database, self.branch)
111
112
 
112
- table_name = self._get_table_name(type(instance))
113
-
114
- # Prepare data for insertion
115
- data = instance.model_dump()
113
+ # Make a copy to avoid modifying the original
114
+ record_data = data.copy()
116
115
 
117
116
  # Generate ID if not provided
118
- if not data.get("id"):
119
- data["id"] = str(uuid.uuid4())
117
+ if not record_data.get("id"):
118
+ record_data["id"] = str(uuid.uuid4())
120
119
 
121
120
  # Set timestamps
122
121
  now = datetime.now()
123
- data["created_at"] = now
124
- data["updated_at"] = now
122
+ record_data["created_at"] = now
123
+ record_data["updated_at"] = now
125
124
 
126
125
  # Build INSERT query
127
- columns = list(data.keys())
126
+ columns = list(record_data.keys())
128
127
  placeholders = [f":{col}" for col in columns]
129
128
  query = f"""
130
129
  INSERT INTO {table_name} ({", ".join(columns)})
@@ -133,17 +132,39 @@ class DataManager:
133
132
 
134
133
  with DatabaseConnection(self.db_path) as conn:
135
134
  try:
136
- conn.execute(query, data)
135
+ conn.execute(query, record_data)
137
136
  conn.commit()
138
137
 
139
- # Return updated instance
140
- return type(instance)(**data)
138
+ # Return the created record data
139
+ return record_data
141
140
  except Exception as e:
142
141
  conn.rollback()
143
142
  if "UNIQUE constraint failed" in str(e):
144
- raise ValueError(f"Record with ID {data['id']} already exists")
143
+ raise ValueError(f"Record with ID {record_data['id']} already exists")
145
144
  raise
146
145
 
146
+ def create(self, instance: T) -> T:
147
+ """Create a new record from a model instance.
148
+
149
+ Args:
150
+ instance: Model instance to create
151
+
152
+ Returns:
153
+ Created model instance with populated ID and timestamps
154
+
155
+ Raises:
156
+ ValueError: If record with same ID already exists
157
+ MaintenanceError: If branch is in maintenance mode
158
+ """
159
+ table_name = self._get_table_name(type(instance))
160
+ data = instance.model_dump()
161
+
162
+ # Use the new create_from_dict method
163
+ created_data = self.create_from_dict(table_name, data)
164
+
165
+ # Return updated instance
166
+ return type(instance)(**created_data)
167
+
147
168
  def save(self, instance: T) -> T:
148
169
  """Save (upsert) a record - insert if new, update if exists.
149
170
 
cinchdb/managers/query.py CHANGED
@@ -7,7 +7,7 @@ from pydantic import BaseModel, ValidationError
7
7
 
8
8
  from cinchdb.core.connection import DatabaseConnection
9
9
  from cinchdb.core.path_utils import get_tenant_db_path
10
- from cinchdb.utils import validate_query_safe, SQLValidationError
10
+ from cinchdb.utils import validate_query_safe
11
11
 
12
12
  T = TypeVar("T", bound=BaseModel)
13
13
 
@@ -33,7 +33,10 @@ class QueryManager:
33
33
  self.db_path = get_tenant_db_path(project_root, database, branch, tenant)
34
34
 
35
35
  def execute(
36
- self, sql: str, params: Optional[Union[tuple, dict]] = None, skip_validation: bool = False
36
+ self,
37
+ sql: str,
38
+ params: Optional[Union[tuple, dict]] = None,
39
+ skip_validation: bool = False,
37
40
  ) -> List[Dict[str, Any]]:
38
41
  """Execute a SQL query and return results as dictionaries.
39
42
 
@@ -52,7 +55,7 @@ class QueryManager:
52
55
  # Validate query unless explicitly skipped
53
56
  if not skip_validation:
54
57
  validate_query_safe(sql)
55
-
58
+
56
59
  # Note: The original code had SELECT-only validation, but we're now more permissive
57
60
  if not sql.strip().upper().startswith("SELECT"):
58
61
  raise ValueError(
@@ -161,7 +164,10 @@ class QueryManager:
161
164
  return results[0] if results else None
162
165
 
163
166
  def execute_non_query(
164
- self, sql: str, params: Optional[Union[tuple, dict]] = None, skip_validation: bool = False
167
+ self,
168
+ sql: str,
169
+ params: Optional[Union[tuple, dict]] = None,
170
+ skip_validation: bool = False,
165
171
  ) -> int:
166
172
  """Execute a non-SELECT SQL query (INSERT, UPDATE, DELETE, etc.).
167
173
 
@@ -172,7 +178,7 @@ class QueryManager:
172
178
 
173
179
  Returns:
174
180
  Number of rows affected
175
-
181
+
176
182
  Raises:
177
183
  SQLValidationError: If query contains restricted operations
178
184
  Exception: If query execution fails
@@ -180,7 +186,7 @@ class QueryManager:
180
186
  # Validate query unless explicitly skipped
181
187
  if not skip_validation:
182
188
  validate_query_safe(sql)
183
-
189
+
184
190
  with DatabaseConnection(self.db_path) as conn:
185
191
  cursor = conn.execute(sql, params)
186
192
  affected_rows = cursor.rowcount