sqlspec 0.13.1__py3-none-any.whl → 0.14.0__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.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (110) hide show
  1. sqlspec/__init__.py +39 -1
  2. sqlspec/adapters/adbc/config.py +4 -40
  3. sqlspec/adapters/adbc/driver.py +29 -16
  4. sqlspec/adapters/aiosqlite/config.py +2 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +2 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +5 -39
  9. sqlspec/adapters/asyncpg/driver.py +41 -18
  10. sqlspec/adapters/bigquery/config.py +2 -43
  11. sqlspec/adapters/bigquery/driver.py +26 -14
  12. sqlspec/adapters/duckdb/config.py +2 -49
  13. sqlspec/adapters/duckdb/driver.py +35 -16
  14. sqlspec/adapters/oracledb/config.py +4 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +2 -55
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +4 -73
  19. sqlspec/adapters/psycopg/driver.py +69 -24
  20. sqlspec/adapters/sqlite/config.py +3 -21
  21. sqlspec/adapters/sqlite/driver.py +50 -26
  22. sqlspec/cli.py +248 -0
  23. sqlspec/config.py +18 -20
  24. sqlspec/driver/_async.py +28 -10
  25. sqlspec/driver/_common.py +5 -4
  26. sqlspec/driver/_sync.py +28 -10
  27. sqlspec/driver/mixins/__init__.py +6 -0
  28. sqlspec/driver/mixins/_cache.py +114 -0
  29. sqlspec/driver/mixins/_pipeline.py +0 -4
  30. sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
  31. sqlspec/driver/mixins/_result_utils.py +0 -2
  32. sqlspec/driver/mixins/_sql_translator.py +0 -2
  33. sqlspec/driver/mixins/_storage.py +4 -18
  34. sqlspec/driver/mixins/_type_coercion.py +0 -2
  35. sqlspec/driver/parameters.py +4 -4
  36. sqlspec/extensions/aiosql/adapter.py +4 -4
  37. sqlspec/extensions/litestar/__init__.py +2 -1
  38. sqlspec/extensions/litestar/cli.py +48 -0
  39. sqlspec/extensions/litestar/plugin.py +3 -0
  40. sqlspec/loader.py +1 -1
  41. sqlspec/migrations/__init__.py +23 -0
  42. sqlspec/migrations/base.py +390 -0
  43. sqlspec/migrations/commands.py +525 -0
  44. sqlspec/migrations/runner.py +215 -0
  45. sqlspec/migrations/tracker.py +153 -0
  46. sqlspec/migrations/utils.py +89 -0
  47. sqlspec/protocols.py +37 -3
  48. sqlspec/statement/builder/__init__.py +8 -8
  49. sqlspec/statement/builder/{column.py → _column.py} +82 -52
  50. sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
  51. sqlspec/statement/builder/_ddl_utils.py +1 -1
  52. sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
  53. sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
  54. sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
  55. sqlspec/statement/builder/_parsing_utils.py +5 -3
  56. sqlspec/statement/builder/{select.py → _select.py} +59 -61
  57. sqlspec/statement/builder/{update.py → _update.py} +2 -2
  58. sqlspec/statement/builder/mixins/__init__.py +24 -30
  59. sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
  60. sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
  61. sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
  62. sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
  63. sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
  64. sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
  65. sqlspec/statement/builder/mixins/_select_operations.py +612 -0
  66. sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
  67. sqlspec/statement/builder/mixins/_where_clause.py +536 -0
  68. sqlspec/statement/cache.py +50 -0
  69. sqlspec/statement/filters.py +37 -8
  70. sqlspec/statement/parameters.py +154 -25
  71. sqlspec/statement/pipelines/__init__.py +1 -1
  72. sqlspec/statement/pipelines/context.py +4 -4
  73. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
  74. sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
  75. sqlspec/statement/pipelines/validators/_performance.py +1 -5
  76. sqlspec/statement/sql.py +246 -176
  77. sqlspec/utils/__init__.py +2 -1
  78. sqlspec/utils/statement_hashing.py +203 -0
  79. sqlspec/utils/type_guards.py +32 -0
  80. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/METADATA +1 -1
  81. sqlspec-0.14.0.dist-info/RECORD +143 -0
  82. sqlspec-0.14.0.dist-info/entry_points.txt +2 -0
  83. sqlspec/service/__init__.py +0 -4
  84. sqlspec/service/_util.py +0 -147
  85. sqlspec/service/pagination.py +0 -26
  86. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  87. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  88. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  89. sqlspec/statement/builder/mixins/_from.py +0 -63
  90. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  91. sqlspec/statement/builder/mixins/_having.py +0 -35
  92. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  93. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  94. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  95. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  96. sqlspec/statement/builder/mixins/_returning.py +0 -37
  97. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  98. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  99. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  100. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  101. sqlspec/statement/builder/mixins/_where.py +0 -401
  102. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  103. sqlspec/statement/parameter_manager.py +0 -220
  104. sqlspec/statement/sql_compiler.py +0 -140
  105. sqlspec-0.13.1.dist-info/RECORD +0 -150
  106. /sqlspec/statement/builder/{base.py → _base.py} +0 -0
  107. /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
  108. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.1.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -13,15 +13,17 @@ from sqlglot.dialects.dialect import DialectType
13
13
  from sqlspec.driver import AsyncDriverAdapterProtocol, SyncDriverAdapterProtocol
14
14
  from sqlspec.driver.connection import managed_transaction_async, managed_transaction_sync
15
15
  from sqlspec.driver.mixins import (
16
+ AsyncAdapterCacheMixin,
16
17
  AsyncPipelinedExecutionMixin,
17
18
  AsyncStorageMixin,
18
19
  SQLTranslatorMixin,
20
+ SyncAdapterCacheMixin,
19
21
  SyncPipelinedExecutionMixin,
20
22
  SyncStorageMixin,
21
23
  ToSchemaMixin,
22
24
  TypeCoercionMixin,
23
25
  )
24
- from sqlspec.driver.parameters import normalize_parameter_sequence
26
+ from sqlspec.driver.parameters import convert_parameter_sequence
25
27
  from sqlspec.exceptions import PipelineExecutionError
26
28
  from sqlspec.statement.parameters import ParameterStyle, ParameterValidator
27
29
  from sqlspec.statement.result import ArrowResult, SQLResult
@@ -43,6 +45,7 @@ PsycopgAsyncConnection = AsyncConnection[PsycopgDictRow]
43
45
 
44
46
  class PsycopgSyncDriver(
45
47
  SyncDriverAdapterProtocol[PsycopgSyncConnection, RowT],
48
+ SyncAdapterCacheMixin,
46
49
  SQLTranslatorMixin,
47
50
  TypeCoercionMixin,
48
51
  SyncStorageMixin,
@@ -57,7 +60,6 @@ class PsycopgSyncDriver(
57
60
  ParameterStyle.NAMED_PYFORMAT,
58
61
  )
59
62
  default_parameter_style: ParameterStyle = ParameterStyle.POSITIONAL_PYFORMAT
60
- __slots__ = ()
61
63
 
62
64
  def __init__(
63
65
  self,
@@ -77,7 +79,7 @@ class PsycopgSyncDriver(
77
79
  self, statement: SQL, connection: Optional[PsycopgSyncConnection] = None, **kwargs: Any
78
80
  ) -> SQLResult[RowT]:
79
81
  if statement.is_script:
80
- sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
82
+ sql, _ = self._get_compiled_sql(statement, ParameterStyle.STATIC)
81
83
  return self._execute_script(sql, connection=connection, **kwargs)
82
84
 
83
85
  detected_styles = set()
@@ -105,7 +107,7 @@ class PsycopgSyncDriver(
105
107
  sql = statement.to_sql(placeholder_style=target_style)
106
108
  params = kwargs_params
107
109
  else:
108
- sql, params = statement.compile(placeholder_style=target_style)
110
+ sql, params = self._get_compiled_sql(statement, target_style)
109
111
  if params is not None:
110
112
  processed_params = [self._process_parameters(param_set) for param_set in params]
111
113
  params = processed_params
@@ -120,7 +122,7 @@ class PsycopgSyncDriver(
120
122
  sql = statement.to_sql(placeholder_style=target_style)
121
123
  params = kwargs_params
122
124
  else:
123
- sql, params = statement.compile(placeholder_style=target_style)
125
+ sql, params = self._get_compiled_sql(statement, target_style)
124
126
  params = self._process_parameters(params)
125
127
 
126
128
  # Fix over-nested parameters for Psycopg
@@ -231,8 +233,8 @@ class PsycopgSyncDriver(
231
233
 
232
234
  with managed_transaction_sync(conn, auto_commit=True) as txn_conn:
233
235
  # Normalize parameter list using consolidated utility
234
- normalized_param_list = normalize_parameter_sequence(param_list)
235
- final_param_list = normalized_param_list or []
236
+ converted_param_list = convert_parameter_sequence(param_list)
237
+ final_param_list = converted_param_list or []
236
238
 
237
239
  with self._get_cursor(txn_conn) as cursor:
238
240
  cursor.executemany(sql, final_param_list)
@@ -256,15 +258,37 @@ class PsycopgSyncDriver(
256
258
  conn = connection if connection is not None else self._connection(None)
257
259
 
258
260
  with managed_transaction_sync(conn, auto_commit=True) as txn_conn, self._get_cursor(txn_conn) as cursor:
259
- cursor.execute(script)
261
+ # Split script into individual statements for validation
262
+ statements = self._split_script_statements(script)
263
+ suppress_warnings = kwargs.get("_suppress_warnings", False)
264
+
265
+ executed_count = 0
266
+ total_rows = 0
267
+ last_status = None
268
+
269
+ # Execute each statement individually for better control and validation
270
+ for statement in statements:
271
+ if statement.strip():
272
+ # Validate each statement unless warnings suppressed
273
+ if not suppress_warnings:
274
+ # Run validation through pipeline
275
+ temp_sql = SQL(statement, config=self.config)
276
+ temp_sql._ensure_processed()
277
+ # Validation errors are logged as warnings by default
278
+
279
+ cursor.execute(statement)
280
+ executed_count += 1
281
+ total_rows += cursor.rowcount or 0
282
+ last_status = cursor.statusmessage
283
+
260
284
  return SQLResult(
261
285
  statement=SQL(script, _dialect=self.dialect).as_script(),
262
286
  data=[],
263
- rows_affected=0,
287
+ rows_affected=total_rows,
264
288
  operation_type="SCRIPT",
265
- metadata={"status_message": cursor.statusmessage or "SCRIPT EXECUTED"},
266
- total_statements=1,
267
- successful_statements=1,
289
+ metadata={"status_message": last_status or "SCRIPT EXECUTED"},
290
+ total_statements=executed_count,
291
+ successful_statements=executed_count,
268
292
  )
269
293
 
270
294
  def _ingest_arrow_table(self, table: "Any", table_name: str, mode: str = "append", **options: Any) -> int:
@@ -471,6 +495,7 @@ class PsycopgSyncDriver(
471
495
 
472
496
  class PsycopgAsyncDriver(
473
497
  AsyncDriverAdapterProtocol[PsycopgAsyncConnection, RowT],
498
+ AsyncAdapterCacheMixin,
474
499
  SQLTranslatorMixin,
475
500
  TypeCoercionMixin,
476
501
  AsyncStorageMixin,
@@ -485,7 +510,6 @@ class PsycopgAsyncDriver(
485
510
  ParameterStyle.NAMED_PYFORMAT,
486
511
  )
487
512
  default_parameter_style: ParameterStyle = ParameterStyle.POSITIONAL_PYFORMAT
488
- __slots__ = ()
489
513
 
490
514
  def __init__(
491
515
  self,
@@ -505,7 +529,7 @@ class PsycopgAsyncDriver(
505
529
  self, statement: SQL, connection: Optional[PsycopgAsyncConnection] = None, **kwargs: Any
506
530
  ) -> SQLResult[RowT]:
507
531
  if statement.is_script:
508
- sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
532
+ sql, _ = self._get_compiled_sql(statement, ParameterStyle.STATIC)
509
533
  return await self._execute_script(sql, connection=connection, **kwargs)
510
534
 
511
535
  detected_styles = set()
@@ -535,8 +559,7 @@ class PsycopgAsyncDriver(
535
559
  sql = statement.to_sql(placeholder_style=target_style)
536
560
  params = kwargs_params
537
561
  else:
538
- sql, _ = statement.compile(placeholder_style=target_style)
539
- params = statement.parameters
562
+ sql, params = self._get_compiled_sql(statement, target_style)
540
563
  if params is not None:
541
564
  processed_params = [self._process_parameters(param_set) for param_set in params]
542
565
  params = processed_params
@@ -560,7 +583,7 @@ class PsycopgAsyncDriver(
560
583
  sql = statement.to_sql(placeholder_style=target_style)
561
584
  params = kwargs_params
562
585
  else:
563
- sql, params = statement.compile(placeholder_style=target_style)
586
+ sql, params = self._get_compiled_sql(statement, target_style)
564
587
  params = self._process_parameters(params)
565
588
 
566
589
  # Fix over-nested parameters for Psycopg
@@ -683,8 +706,8 @@ class PsycopgAsyncDriver(
683
706
 
684
707
  async with managed_transaction_async(conn, auto_commit=True) as txn_conn:
685
708
  # Normalize parameter list using consolidated utility
686
- normalized_param_list = normalize_parameter_sequence(param_list)
687
- final_param_list = normalized_param_list or []
709
+ converted_param_list = convert_parameter_sequence(param_list)
710
+ final_param_list = converted_param_list or []
688
711
 
689
712
  async with txn_conn.cursor() as cursor:
690
713
  await cursor.executemany(cast("Query", sql), final_param_list)
@@ -703,15 +726,37 @@ class PsycopgAsyncDriver(
703
726
  conn = connection if connection is not None else self._connection(None)
704
727
 
705
728
  async with managed_transaction_async(conn, auto_commit=True) as txn_conn, txn_conn.cursor() as cursor:
706
- await cursor.execute(cast("Query", script))
729
+ # Split script into individual statements for validation
730
+ statements = self._split_script_statements(script)
731
+ suppress_warnings = kwargs.get("_suppress_warnings", False)
732
+
733
+ executed_count = 0
734
+ total_rows = 0
735
+ last_status = None
736
+
737
+ # Execute each statement individually for better control and validation
738
+ for statement in statements:
739
+ if statement.strip():
740
+ # Validate each statement unless warnings suppressed
741
+ if not suppress_warnings:
742
+ # Run validation through pipeline
743
+ temp_sql = SQL(statement, config=self.config)
744
+ temp_sql._ensure_processed()
745
+ # Validation errors are logged as warnings by default
746
+
747
+ await cursor.execute(cast("Query", statement))
748
+ executed_count += 1
749
+ total_rows += cursor.rowcount or 0
750
+ last_status = cursor.statusmessage
751
+
707
752
  return SQLResult(
708
753
  statement=SQL(script, _dialect=self.dialect).as_script(),
709
754
  data=[],
710
- rows_affected=0,
755
+ rows_affected=total_rows,
711
756
  operation_type="SCRIPT",
712
- metadata={"status_message": cursor.statusmessage or "SCRIPT EXECUTED"},
713
- total_statements=1,
714
- successful_statements=1,
757
+ metadata={"status_message": last_status or "SCRIPT EXECUTED"},
758
+ total_statements=executed_count,
759
+ successful_statements=executed_count,
715
760
  )
716
761
 
717
762
  async def _fetch_arrow_table(self, sql: SQL, connection: "Optional[Any]" = None, **kwargs: Any) -> "ArrowResult":
@@ -13,7 +13,6 @@ from sqlspec.typing import DictRow
13
13
  if TYPE_CHECKING:
14
14
  from collections.abc import Generator
15
15
 
16
- from sqlglot.dialects.dialect import DialectType
17
16
 
18
17
  logger = logging.getLogger(__name__)
19
18
 
@@ -36,26 +35,10 @@ __all__ = ("CONNECTION_FIELDS", "SqliteConfig", "sqlite3")
36
35
  class SqliteConfig(NoPoolSyncConfig[SqliteConnection, SqliteDriver]):
37
36
  """Configuration for SQLite database connections with direct field-based configuration."""
38
37
 
39
- __slots__ = (
40
- "_dialect",
41
- "cached_statements",
42
- "check_same_thread",
43
- "database",
44
- "default_row_type",
45
- "detect_types",
46
- "extras",
47
- "factory",
48
- "isolation_level",
49
- "pool_instance",
50
- "statement_config",
51
- "timeout",
52
- "uri",
53
- )
54
-
55
38
  driver_type: type[SqliteDriver] = SqliteDriver
56
39
  connection_type: type[SqliteConnection] = SqliteConnection
57
40
  supported_parameter_styles: ClassVar[tuple[str, ...]] = ("qmark", "named_colon")
58
- preferred_parameter_style: ClassVar[str] = "qmark"
41
+ default_parameter_style: ClassVar[str] = "qmark"
59
42
 
60
43
  def __init__(
61
44
  self,
@@ -65,7 +48,7 @@ class SqliteConfig(NoPoolSyncConfig[SqliteConnection, SqliteDriver]):
65
48
  # SQLite connection parameters
66
49
  timeout: Optional[float] = None,
67
50
  detect_types: Optional[int] = None,
68
- isolation_level: Optional[Union[str, None]] = None,
51
+ isolation_level: Union[None, str] = None,
69
52
  check_same_thread: Optional[bool] = None,
70
53
  factory: Optional[type[SqliteConnection]] = None,
71
54
  cached_statements: Optional[int] = None,
@@ -106,7 +89,6 @@ class SqliteConfig(NoPoolSyncConfig[SqliteConnection, SqliteDriver]):
106
89
  # Store other config
107
90
  self.statement_config = statement_config or SQLConfig()
108
91
  self.default_row_type = default_row_type
109
- self._dialect: DialectType = None
110
92
  super().__init__()
111
93
 
112
94
  @property
@@ -169,6 +151,6 @@ class SqliteConfig(NoPoolSyncConfig[SqliteConnection, SqliteDriver]):
169
151
  statement_config = replace(
170
152
  statement_config,
171
153
  allowed_parameter_styles=self.supported_parameter_styles,
172
- target_parameter_style=self.preferred_parameter_style,
154
+ default_parameter_style=self.default_parameter_style,
173
155
  )
174
156
  yield self.driver_type(connection=connection, config=statement_config)
@@ -12,12 +12,14 @@ from sqlspec.driver import SyncDriverAdapterProtocol
12
12
  from sqlspec.driver.connection import managed_transaction_sync
13
13
  from sqlspec.driver.mixins import (
14
14
  SQLTranslatorMixin,
15
+ SyncAdapterCacheMixin,
15
16
  SyncPipelinedExecutionMixin,
17
+ SyncQueryMixin,
16
18
  SyncStorageMixin,
17
19
  ToSchemaMixin,
18
20
  TypeCoercionMixin,
19
21
  )
20
- from sqlspec.driver.parameters import normalize_parameter_sequence
22
+ from sqlspec.driver.parameters import convert_parameter_sequence
21
23
  from sqlspec.statement.parameters import ParameterStyle, ParameterValidator
22
24
  from sqlspec.statement.result import SQLResult
23
25
  from sqlspec.statement.sql import SQL, SQLConfig
@@ -37,10 +39,12 @@ SqliteConnection: TypeAlias = sqlite3.Connection
37
39
 
38
40
  class SqliteDriver(
39
41
  SyncDriverAdapterProtocol[SqliteConnection, RowT],
42
+ SyncAdapterCacheMixin,
40
43
  SQLTranslatorMixin,
41
44
  TypeCoercionMixin,
42
45
  SyncStorageMixin,
43
46
  SyncPipelinedExecutionMixin,
47
+ SyncQueryMixin,
44
48
  ToSchemaMixin,
45
49
  ):
46
50
  """SQLite Sync Driver Adapter with Arrow/Parquet export support.
@@ -49,8 +53,6 @@ class SqliteDriver(
49
53
  instrumentation standards following the psycopg pattern.
50
54
  """
51
55
 
52
- __slots__ = ()
53
-
54
56
  dialect: "DialectType" = "sqlite"
55
57
  supported_parameter_styles: "tuple[ParameterStyle, ...]" = (ParameterStyle.QMARK, ParameterStyle.NAMED_COLON)
56
58
  default_parameter_style: ParameterStyle = ParameterStyle.QMARK
@@ -106,7 +108,7 @@ class SqliteDriver(
106
108
  self, statement: SQL, connection: Optional[SqliteConnection] = None, **kwargs: Any
107
109
  ) -> SQLResult[RowT]:
108
110
  if statement.is_script:
109
- sql, _ = statement.compile(placeholder_style=ParameterStyle.STATIC)
111
+ sql, _ = self._get_compiled_sql(statement, ParameterStyle.STATIC)
110
112
  return self._execute_script(sql, connection=connection, statement=statement, **kwargs)
111
113
 
112
114
  detected_styles = set()
@@ -126,17 +128,17 @@ class SqliteDriver(
126
128
  target_style = self.default_parameter_style
127
129
  elif detected_styles:
128
130
  # Single style detected - use it if supported
129
- single_style = next(iter(detected_styles))
130
- if single_style in self.supported_parameter_styles:
131
- target_style = single_style
131
+ detected_style = next(iter(detected_styles))
132
+ if detected_style.value in self.supported_parameter_styles:
133
+ target_style = detected_style
132
134
  else:
133
135
  target_style = self.default_parameter_style
134
136
 
135
137
  if statement.is_many:
136
- sql, params = statement.compile(placeholder_style=target_style)
138
+ sql, params = self._get_compiled_sql(statement, target_style)
137
139
  return self._execute_many(sql, params, connection=connection, statement=statement, **kwargs)
138
140
 
139
- sql, params = statement.compile(placeholder_style=target_style)
141
+ sql, params = self._get_compiled_sql(statement, target_style)
140
142
 
141
143
  params = self._process_parameters(params)
142
144
 
@@ -153,18 +155,18 @@ class SqliteDriver(
153
155
  # Use provided connection or driver's default connection
154
156
  conn = connection if connection is not None else self._connection(None)
155
157
  with managed_transaction_sync(conn, auto_commit=True) as txn_conn, self._get_cursor(txn_conn) as cursor:
156
- # Normalize parameters using consolidated utility
157
- normalized_params_list = normalize_parameter_sequence(parameters)
158
+ # Convert parameters using consolidated utility
159
+ converted_params_list = convert_parameter_sequence(parameters)
158
160
  params_for_execute: Any
159
- if normalized_params_list and len(normalized_params_list) == 1:
161
+ if converted_params_list and len(converted_params_list) == 1:
160
162
  # Single parameter should be tuple for SQLite
161
- if not isinstance(normalized_params_list[0], (tuple, list, dict)):
162
- params_for_execute = (normalized_params_list[0],)
163
+ if not isinstance(converted_params_list[0], (tuple, list, dict)):
164
+ params_for_execute = (converted_params_list[0],)
163
165
  else:
164
- params_for_execute = normalized_params_list[0]
166
+ params_for_execute = converted_params_list[0]
165
167
  else:
166
168
  # Multiple parameters
167
- params_for_execute = tuple(normalized_params_list) if normalized_params_list else ()
169
+ params_for_execute = tuple(converted_params_list) if converted_params_list else ()
168
170
 
169
171
  cursor.execute(sql, params_for_execute)
170
172
  if self.returns_rows(statement.expression):
@@ -199,10 +201,10 @@ class SqliteDriver(
199
201
  conn = connection if connection is not None else self._connection(None)
200
202
  with managed_transaction_sync(conn, auto_commit=True) as txn_conn:
201
203
  # Normalize parameter list using consolidated utility
202
- normalized_param_list = normalize_parameter_sequence(param_list)
204
+ converted_param_list = convert_parameter_sequence(param_list)
203
205
  formatted_params: list[tuple[Any, ...]] = []
204
- if normalized_param_list:
205
- for param_set in normalized_param_list:
206
+ if converted_param_list:
207
+ for param_set in converted_param_list:
206
208
  if isinstance(param_set, (list, tuple)):
207
209
  formatted_params.append(tuple(param_set))
208
210
  elif param_set is None:
@@ -227,12 +229,34 @@ class SqliteDriver(
227
229
  def _execute_script(
228
230
  self, script: str, connection: Optional[SqliteConnection] = None, statement: Optional[SQL] = None, **kwargs: Any
229
231
  ) -> SQLResult[RowT]:
230
- """Execute a script on the SQLite connection."""
231
- # Use provided connection or driver's default connection
232
+ """Execute script using splitter for per-statement validation."""
233
+ from sqlspec.statement.splitter import split_sql_script
234
+
232
235
  conn = connection if connection is not None else self._connection(None)
236
+ statements = split_sql_script(script, dialect="sqlite")
237
+
238
+ total_rows = 0
239
+ successful = 0
240
+ suppress_warnings = kwargs.get("_suppress_warnings", False)
241
+
233
242
  with self._get_cursor(conn) as cursor:
234
- cursor.executescript(script)
235
- # executescript doesn't auto-commit in some cases - force commit
243
+ for stmt in statements:
244
+ try:
245
+ # Validate each statement unless warnings suppressed
246
+ if not suppress_warnings and statement:
247
+ # Run validation through pipeline
248
+ temp_sql = SQL(stmt, config=statement._config)
249
+ temp_sql._ensure_processed()
250
+ # Validation errors are logged as warnings by default
251
+
252
+ cursor.execute(stmt)
253
+ successful += 1
254
+ total_rows += cursor.rowcount or 0
255
+ except Exception as e: # noqa: PERF203
256
+ if not kwargs.get("continue_on_error", False):
257
+ raise
258
+ logger.warning("Script statement failed: %s", e)
259
+
236
260
  conn.commit()
237
261
 
238
262
  if statement is None:
@@ -241,10 +265,10 @@ class SqliteDriver(
241
265
  return SQLResult(
242
266
  statement=statement,
243
267
  data=[],
244
- rows_affected=-1, # Unknown for scripts
268
+ rows_affected=total_rows,
245
269
  operation_type="SCRIPT",
246
- total_statements=-1, # SQLite doesn't provide this info
247
- successful_statements=-1,
270
+ total_statements=len(statements),
271
+ successful_statements=successful,
248
272
  metadata={"status_message": "SCRIPT EXECUTED"},
249
273
  )
250
274
 
sqlspec/cli.py ADDED
@@ -0,0 +1,248 @@
1
+ import sys
2
+ from collections.abc import Sequence
3
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
4
+
5
+ if TYPE_CHECKING:
6
+ from click import Group
7
+
8
+ from sqlspec.config import AsyncDatabaseConfig, SyncDatabaseConfig
9
+
10
+ __all__ = ("add_migration_commands", "get_sqlspec_group")
11
+
12
+
13
+ def get_sqlspec_group() -> "Group":
14
+ """Get the SQLSpec CLI group.
15
+
16
+ Raises:
17
+ MissingDependencyError: If the `click` package is not installed.
18
+
19
+ Returns:
20
+ The SQLSpec CLI group.
21
+ """
22
+ from sqlspec.exceptions import MissingDependencyError
23
+
24
+ try:
25
+ import rich_click as click
26
+ except ImportError:
27
+ try:
28
+ import click # type: ignore[no-redef]
29
+ except ImportError as e:
30
+ raise MissingDependencyError(package="click", install_package="cli") from e
31
+
32
+ @click.group(name="sqlspec")
33
+ @click.option(
34
+ "--config",
35
+ help="Dotted path to SQLAlchemy config(s) (e.g. 'myapp.config.sqlspec_configs')",
36
+ required=True,
37
+ type=str,
38
+ )
39
+ @click.pass_context
40
+ def sqlspec_group(ctx: "click.Context", config: str) -> None:
41
+ """SQLSpec CLI commands."""
42
+ from rich import get_console
43
+
44
+ from sqlspec.utils import module_loader
45
+
46
+ console = get_console()
47
+ ctx.ensure_object(dict)
48
+ try:
49
+ config_instance = module_loader.import_string(config)
50
+ if isinstance(config_instance, Sequence):
51
+ ctx.obj["configs"] = config_instance
52
+ else:
53
+ ctx.obj["configs"] = [config_instance]
54
+ except ImportError as e:
55
+ console.print(f"[red]Error loading config: {e}[/]")
56
+ ctx.exit(1)
57
+
58
+ return sqlspec_group
59
+
60
+
61
+ def add_migration_commands(database_group: Optional["Group"] = None) -> "Group":
62
+ """Add migration commands to the database group.
63
+
64
+ Args:
65
+ database_group: The database group to add the commands to.
66
+
67
+ Raises:
68
+ MissingDependencyError: If the `click` package is not installed.
69
+
70
+ Returns:
71
+ The database group with the migration commands added.
72
+ """
73
+ from sqlspec.exceptions import MissingDependencyError
74
+
75
+ try:
76
+ import rich_click as click
77
+ except ImportError:
78
+ try:
79
+ import click # type: ignore[no-redef]
80
+ except ImportError as e:
81
+ raise MissingDependencyError(package="click", install_package="cli") from e
82
+ from rich import get_console
83
+
84
+ console = get_console()
85
+
86
+ if database_group is None:
87
+ database_group = get_sqlspec_group()
88
+
89
+ bind_key_option = click.option(
90
+ "--bind-key", help="Specify which SQLAlchemy config to use by bind key", type=str, default=None
91
+ )
92
+ verbose_option = click.option("--verbose", help="Enable verbose output.", type=bool, default=False, is_flag=True)
93
+ no_prompt_option = click.option(
94
+ "--no-prompt",
95
+ help="Do not prompt for confirmation before executing the command.",
96
+ type=bool,
97
+ default=False,
98
+ required=False,
99
+ show_default=True,
100
+ is_flag=True,
101
+ )
102
+
103
+ def get_config_by_bind_key(
104
+ ctx: "click.Context", bind_key: Optional[str]
105
+ ) -> "Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]":
106
+ """Get the SQLAlchemy config for the specified bind key.
107
+
108
+ Args:
109
+ ctx: The click context.
110
+ bind_key: The bind key to get the config for.
111
+
112
+ Returns:
113
+ The SQLAlchemy config for the specified bind key.
114
+ """
115
+ configs = ctx.obj["configs"]
116
+ if bind_key is None:
117
+ return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", configs[0])
118
+
119
+ for config in configs:
120
+ # Check if config has a name or identifier attribute
121
+ config_name = getattr(config, "name", None) or getattr(config, "bind_key", None)
122
+ if config_name == bind_key:
123
+ return cast("Union[AsyncDatabaseConfig[Any, Any, Any], SyncDatabaseConfig[Any, Any, Any]]", config)
124
+
125
+ console.print(f"[red]No config found for bind key: {bind_key}[/]")
126
+ sys.exit(1)
127
+
128
+ @database_group.command(name="show-current-revision", help="Shows the current revision for the database.")
129
+ @bind_key_option
130
+ @verbose_option
131
+ def show_database_revision(bind_key: Optional[str], verbose: bool) -> None: # pyright: ignore[reportUnusedFunction]
132
+ """Show current database revision."""
133
+ from sqlspec.migrations.commands import MigrationCommands
134
+
135
+ ctx = click.get_current_context()
136
+ console.rule("[yellow]Listing current revision[/]", align="left")
137
+ sqlspec_config = get_config_by_bind_key(ctx, bind_key)
138
+ migration_commands = MigrationCommands(config=sqlspec_config)
139
+ migration_commands.current(verbose=verbose)
140
+
141
+ @database_group.command(name="downgrade", help="Downgrade database to a specific revision.")
142
+ @bind_key_option
143
+ @no_prompt_option
144
+ @click.argument("revision", type=str, default="-1")
145
+ def downgrade_database( # pyright: ignore[reportUnusedFunction]
146
+ bind_key: Optional[str], revision: str, no_prompt: bool
147
+ ) -> None:
148
+ """Downgrade the database to the latest revision."""
149
+ from rich.prompt import Confirm
150
+
151
+ from sqlspec.migrations.commands import MigrationCommands
152
+
153
+ ctx = click.get_current_context()
154
+ console.rule("[yellow]Starting database downgrade process[/]", align="left")
155
+ input_confirmed = (
156
+ True
157
+ if no_prompt
158
+ else Confirm.ask(f"Are you sure you want to downgrade the database to the `{revision}` revision?")
159
+ )
160
+ if input_confirmed:
161
+ sqlspec_config = get_config_by_bind_key(ctx, bind_key)
162
+ migration_commands = MigrationCommands(config=sqlspec_config)
163
+ migration_commands.downgrade(revision=revision)
164
+
165
+ @database_group.command(name="upgrade", help="Upgrade database to a specific revision.")
166
+ @bind_key_option
167
+ @no_prompt_option
168
+ @click.argument("revision", type=str, default="head")
169
+ def upgrade_database( # pyright: ignore[reportUnusedFunction]
170
+ bind_key: Optional[str], revision: str, no_prompt: bool
171
+ ) -> None:
172
+ """Upgrade the database to the latest revision."""
173
+ from rich.prompt import Confirm
174
+
175
+ from sqlspec.migrations.commands import MigrationCommands
176
+
177
+ ctx = click.get_current_context()
178
+ console.rule("[yellow]Starting database upgrade process[/]", align="left")
179
+ input_confirmed = (
180
+ True
181
+ if no_prompt
182
+ else Confirm.ask(f"[bold]Are you sure you want migrate the database to the `{revision}` revision?[/]")
183
+ )
184
+ if input_confirmed:
185
+ sqlspec_config = get_config_by_bind_key(ctx, bind_key)
186
+ migration_commands = MigrationCommands(config=sqlspec_config)
187
+ migration_commands.upgrade(revision=revision)
188
+
189
+ @database_group.command(help="Stamp the revision table with the given revision")
190
+ @click.argument("revision", type=str)
191
+ @bind_key_option
192
+ def stamp(bind_key: Optional[str], revision: str) -> None: # pyright: ignore[reportUnusedFunction]
193
+ """Stamp the revision table with the given revision."""
194
+ from sqlspec.migrations.commands import MigrationCommands
195
+
196
+ ctx = click.get_current_context()
197
+ sqlspec_config = get_config_by_bind_key(ctx, bind_key)
198
+ migration_commands = MigrationCommands(config=sqlspec_config)
199
+ migration_commands.stamp(revision=revision)
200
+
201
+ @database_group.command(name="init", help="Initialize migrations for the project.")
202
+ @bind_key_option
203
+ @click.argument("directory", default=None, required=False)
204
+ @click.option("--package", is_flag=True, default=True, help="Create `__init__.py` for created folder")
205
+ @no_prompt_option
206
+ def init_sqlspec( # pyright: ignore[reportUnusedFunction]
207
+ bind_key: Optional[str], directory: Optional[str], package: bool, no_prompt: bool
208
+ ) -> None:
209
+ """Initialize the database migrations."""
210
+ from rich.prompt import Confirm
211
+
212
+ from sqlspec.migrations.commands import MigrationCommands
213
+
214
+ ctx = click.get_current_context()
215
+ console.rule("[yellow]Initializing database migrations.", align="left")
216
+ input_confirmed = (
217
+ True if no_prompt else Confirm.ask("[bold]Are you sure you want initialize migrations for the project?[/]")
218
+ )
219
+ if input_confirmed:
220
+ configs = [get_config_by_bind_key(ctx, bind_key)] if bind_key is not None else ctx.obj["configs"]
221
+ for config in configs:
222
+ migration_config = getattr(config, "migration_config", {})
223
+ directory = migration_config.get("script_location", "migrations") if directory is None else directory
224
+ migration_commands = MigrationCommands(config=config)
225
+ migration_commands.init(directory=cast("str", directory), package=package)
226
+
227
+ @database_group.command(name="make-migrations", help="Create a new migration revision.")
228
+ @bind_key_option
229
+ @click.option("-m", "--message", default=None, help="Revision message")
230
+ @no_prompt_option
231
+ def create_revision( # pyright: ignore[reportUnusedFunction]
232
+ bind_key: Optional[str], message: Optional[str], no_prompt: bool
233
+ ) -> None:
234
+ """Create a new database revision."""
235
+ from rich.prompt import Prompt
236
+
237
+ from sqlspec.migrations.commands import MigrationCommands
238
+
239
+ ctx = click.get_current_context()
240
+ console.rule("[yellow]Creating new migration revision[/]", align="left")
241
+ if message is None:
242
+ message = "new migration" if no_prompt else Prompt.ask("Please enter a message describing this revision")
243
+
244
+ sqlspec_config = get_config_by_bind_key(ctx, bind_key)
245
+ migration_commands = MigrationCommands(config=sqlspec_config)
246
+ migration_commands.revision(message=message)
247
+
248
+ return database_group