sqlspec 0.13.0__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 +15 -20
  5. sqlspec/adapters/aiosqlite/driver.py +36 -18
  6. sqlspec/adapters/asyncmy/config.py +16 -33
  7. sqlspec/adapters/asyncmy/driver.py +23 -16
  8. sqlspec/adapters/asyncpg/config.py +19 -61
  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 +30 -83
  15. sqlspec/adapters/oracledb/driver.py +54 -27
  16. sqlspec/adapters/psqlpy/config.py +17 -57
  17. sqlspec/adapters/psqlpy/driver.py +28 -8
  18. sqlspec/adapters/psycopg/config.py +30 -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.0.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.0.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.0.dist-info → sqlspec-0.14.0.dist-info}/WHEEL +0 -0
  109. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/LICENSE +0 -0
  110. {sqlspec-0.13.0.dist-info → sqlspec-0.14.0.dist-info}/licenses/NOTICE +0 -0
@@ -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
sqlspec/config.py CHANGED
@@ -47,10 +47,6 @@ logger = get_logger("config")
47
47
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
48
48
  """Protocol defining the interface for database configurations."""
49
49
 
50
- # Note: __slots__ cannot be used with dataclass fields in Python < 3.10
51
- # Concrete subclasses can still use __slots__ for any additional attributes
52
- __slots__ = ()
53
-
54
50
  is_async: "ClassVar[bool]" = field(init=False, default=False)
55
51
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
56
52
  supports_native_arrow_import: "ClassVar[bool]" = field(init=False, default=False)
@@ -61,12 +57,24 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
61
57
  driver_type: "type[DriverT]" = field(init=False, repr=False, hash=False, compare=False)
62
58
  pool_instance: "Optional[PoolT]" = field(default=None)
63
59
  default_row_type: "type[Any]" = field(init=False)
60
+ migration_config: "dict[str, Any]" = field(default_factory=dict)
61
+ """Migration configuration settings."""
64
62
  _dialect: "DialectType" = field(default=None, init=False, repr=False, hash=False, compare=False)
65
63
 
64
+ # Adapter-level cache configuration
65
+ enable_adapter_cache: bool = field(default=True)
66
+ """Enable adapter-level SQL compilation caching."""
67
+ adapter_cache_size: int = field(default=500)
68
+ """Maximum number of compiled SQL statements to cache per adapter."""
69
+ enable_prepared_statements: bool = field(default=False)
70
+ """Enable prepared statement pooling for supported databases."""
71
+ prepared_statement_cache_size: int = field(default=100)
72
+ """Maximum number of prepared statements to maintain."""
73
+
66
74
  supported_parameter_styles: "ClassVar[tuple[str, ...]]" = ()
67
75
  """Parameter styles supported by this database adapter (e.g., ('qmark', 'named_colon'))."""
68
76
 
69
- preferred_parameter_style: "ClassVar[str]" = "none"
77
+ default_parameter_style: "ClassVar[str]" = "none"
70
78
  """The preferred/native parameter style for this database."""
71
79
 
72
80
  def __hash__(self) -> int:
@@ -85,7 +93,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
85
93
  The SQL dialect type for this database.
86
94
  """
87
95
  if self._dialect is None:
88
- self._dialect = self._get_dialect() # type: ignore[misc]
96
+ self._dialect = self._get_dialect()
89
97
  return self._dialect
90
98
 
91
99
  def _get_dialect(self) -> "DialectType":
@@ -177,8 +185,6 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
177
185
  class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
178
186
  """Base class for a sync database configurations that do not implement a pool."""
179
187
 
180
- __slots__ = ()
181
-
182
188
  is_async: "ClassVar[bool]" = field(init=False, default=False)
183
189
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
184
190
  pool_instance: None = None
@@ -209,8 +215,6 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
209
215
  class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
210
216
  """Base class for an async database configurations that do not implement a pool."""
211
217
 
212
- __slots__ = ()
213
-
214
218
  is_async: "ClassVar[bool]" = field(init=False, default=True)
215
219
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=False)
216
220
  pool_instance: None = None
@@ -241,15 +245,11 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
241
245
  class GenericPoolConfig:
242
246
  """Generic Database Pool Configuration."""
243
247
 
244
- __slots__ = ()
245
-
246
248
 
247
249
  @dataclass
248
250
  class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
249
251
  """Generic Sync Database Configuration."""
250
252
 
251
- __slots__ = ()
252
-
253
253
  is_async: "ClassVar[bool]" = field(init=False, default=False)
254
254
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=True)
255
255
 
@@ -261,7 +261,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
261
261
  """
262
262
  if self.pool_instance is not None:
263
263
  return self.pool_instance
264
- self.pool_instance = self._create_pool() # type: ignore[misc]
264
+ self.pool_instance = self._create_pool()
265
265
  return self.pool_instance
266
266
 
267
267
  def close_pool(self) -> None:
@@ -271,7 +271,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
271
271
  def provide_pool(self, *args: Any, **kwargs: Any) -> PoolT:
272
272
  """Provide pool instance."""
273
273
  if self.pool_instance is None:
274
- self.pool_instance = self.create_pool() # type: ignore[misc]
274
+ self.pool_instance = self.create_pool()
275
275
  return self.pool_instance
276
276
 
277
277
  def create_connection(self) -> ConnectionT:
@@ -301,8 +301,6 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
301
301
  class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
302
302
  """Generic Async Database Configuration."""
303
303
 
304
- __slots__ = ()
305
-
306
304
  is_async: "ClassVar[bool]" = field(init=False, default=True)
307
305
  supports_connection_pooling: "ClassVar[bool]" = field(init=False, default=True)
308
306
 
@@ -314,7 +312,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
314
312
  """
315
313
  if self.pool_instance is not None:
316
314
  return self.pool_instance
317
- self.pool_instance = await self._create_pool() # type: ignore[misc]
315
+ self.pool_instance = await self._create_pool()
318
316
  return self.pool_instance
319
317
 
320
318
  async def close_pool(self) -> None:
@@ -324,7 +322,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
324
322
  async def provide_pool(self, *args: Any, **kwargs: Any) -> PoolT:
325
323
  """Provide pool instance."""
326
324
  if self.pool_instance is None:
327
- self.pool_instance = await self.create_pool() # type: ignore[misc]
325
+ self.pool_instance = await self.create_pool()
328
326
  return self.pool_instance
329
327
 
330
328
  async def create_connection(self) -> ConnectionT:
sqlspec/driver/_async.py CHANGED
@@ -207,17 +207,23 @@ class AsyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
207
207
  _config: "Optional[SQLConfig]" = None,
208
208
  **kwargs: Any,
209
209
  ) -> "SQLResult[RowT]":
210
- _filters, param_sequence = process_execute_many_parameters(parameters)
210
+ """Execute statement multiple times with different parameters.
211
211
 
212
- # For execute_many, disable transformations to prevent literal extraction
213
- # since the SQL already has placeholders for bulk operations
214
- many_config = _config or self.config
215
- if many_config.enable_transformations:
216
- from dataclasses import replace
212
+ Now passes first parameter set through pipeline to enable
213
+ literal extraction and consistent parameter processing.
214
+ """
215
+ filters, param_sequence = process_execute_many_parameters(parameters)
216
+
217
+ # Process first parameter set through pipeline for literal extraction
218
+ first_params = param_sequence[0] if param_sequence else None
217
219
 
218
- many_config = replace(many_config, enable_transformations=False)
220
+ # Build statement with first params to trigger pipeline processing
221
+ sql_statement = self._build_statement(
222
+ statement, first_params, *filters, _config=_config or self.config, **kwargs
223
+ )
219
224
 
220
- sql_statement = self._build_statement(statement, _config=many_config, **kwargs).as_many(param_sequence)
225
+ # Mark as many with full sequence
226
+ sql_statement = sql_statement.as_many(param_sequence)
221
227
 
222
228
  return await self._execute_statement(
223
229
  statement=sql_statement, connection=self._connection(_connection), **kwargs
@@ -230,14 +236,26 @@ class AsyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
230
236
  *parameters: "Union[StatementParameters, StatementFilter]",
231
237
  _connection: "Optional[ConnectionT]" = None,
232
238
  _config: "Optional[SQLConfig]" = None,
239
+ _suppress_warnings: bool = False, # New parameter for migrations
233
240
  **kwargs: Any,
234
241
  ) -> "SQLResult[RowT]":
242
+ """Execute a multi-statement script.
243
+
244
+ By default, validates each statement and logs warnings for dangerous
245
+ operations. Use _suppress_warnings=True for migrations and admin scripts.
246
+ """
235
247
  script_config = _config or self.config
236
- if script_config.enable_validation:
237
- script_config = replace(script_config, enable_validation=False, strict_mode=False)
248
+
249
+ # Keep validation enabled by default
250
+ # Validators will log warnings for dangerous operations
238
251
 
239
252
  sql_statement = self._build_statement(statement, *parameters, _config=script_config, **kwargs)
240
253
  sql_statement = sql_statement.as_script()
254
+
255
+ # Pass suppress warnings flag to execution
256
+ if _suppress_warnings:
257
+ kwargs["_suppress_warnings"] = True
258
+
241
259
  return await self._execute_statement(
242
260
  statement=sql_statement, connection=self._connection(_connection), **kwargs
243
261
  )
sqlspec/driver/_common.py CHANGED
@@ -9,7 +9,7 @@ import sqlglot
9
9
  from sqlglot import exp
10
10
  from sqlglot.tokens import TokenType
11
11
 
12
- from sqlspec.driver.parameters import normalize_parameter_sequence
12
+ from sqlspec.driver.parameters import convert_parameter_sequence
13
13
  from sqlspec.exceptions import NotFoundError
14
14
  from sqlspec.statement import SQLConfig
15
15
  from sqlspec.statement.parameters import ParameterStyle, ParameterValidator, TypedParameter
@@ -60,6 +60,7 @@ class CommonDriverAttributesMixin(ABC, Generic[ConnectionT, RowT]):
60
60
  config: SQL statement configuration
61
61
  default_row_type: Default row type for results (DictRow, TupleRow, etc.)
62
62
  """
63
+ super().__init__()
63
64
  self.connection = connection
64
65
  self.config = config or SQLConfig()
65
66
  self.default_row_type = default_row_type or dict[str, Any]
@@ -335,11 +336,11 @@ class CommonDriverAttributesMixin(ABC, Generic[ConnectionT, RowT]):
335
336
  Parameters with TypedParameter objects unwrapped to primitive values
336
337
  """
337
338
 
338
- normalized = normalize_parameter_sequence(parameters)
339
- if not normalized:
339
+ converted = convert_parameter_sequence(parameters)
340
+ if not converted:
340
341
  return []
341
342
 
342
- return [self._coerce_parameter(p) if isinstance(p, TypedParameter) else p for p in normalized]
343
+ return [self._coerce_parameter(p) if isinstance(p, TypedParameter) else p for p in converted]
343
344
 
344
345
  def _prepare_driver_parameters_many(self, parameters: Any) -> "list[Any]":
345
346
  """Prepare parameter sequences for executemany operations.