sqlspec 0.13.1__py3-none-any.whl → 0.14.1__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.
- sqlspec/__init__.py +39 -1
- sqlspec/__main__.py +12 -0
- sqlspec/adapters/adbc/config.py +16 -40
- sqlspec/adapters/adbc/driver.py +43 -16
- sqlspec/adapters/adbc/transformers.py +108 -0
- sqlspec/adapters/aiosqlite/config.py +2 -20
- sqlspec/adapters/aiosqlite/driver.py +36 -18
- sqlspec/adapters/asyncmy/config.py +2 -33
- sqlspec/adapters/asyncmy/driver.py +23 -16
- sqlspec/adapters/asyncpg/config.py +5 -39
- sqlspec/adapters/asyncpg/driver.py +41 -18
- sqlspec/adapters/bigquery/config.py +2 -43
- sqlspec/adapters/bigquery/driver.py +26 -14
- sqlspec/adapters/duckdb/config.py +2 -49
- sqlspec/adapters/duckdb/driver.py +35 -16
- sqlspec/adapters/oracledb/config.py +4 -83
- sqlspec/adapters/oracledb/driver.py +54 -27
- sqlspec/adapters/psqlpy/config.py +2 -55
- sqlspec/adapters/psqlpy/driver.py +28 -8
- sqlspec/adapters/psycopg/config.py +4 -73
- sqlspec/adapters/psycopg/driver.py +69 -24
- sqlspec/adapters/sqlite/config.py +3 -21
- sqlspec/adapters/sqlite/driver.py +50 -26
- sqlspec/cli.py +248 -0
- sqlspec/config.py +18 -20
- sqlspec/driver/_async.py +28 -10
- sqlspec/driver/_common.py +5 -4
- sqlspec/driver/_sync.py +28 -10
- sqlspec/driver/mixins/__init__.py +6 -0
- sqlspec/driver/mixins/_cache.py +114 -0
- sqlspec/driver/mixins/_pipeline.py +0 -4
- sqlspec/{service/base.py → driver/mixins/_query_tools.py} +86 -421
- sqlspec/driver/mixins/_result_utils.py +0 -2
- sqlspec/driver/mixins/_sql_translator.py +0 -2
- sqlspec/driver/mixins/_storage.py +4 -18
- sqlspec/driver/mixins/_type_coercion.py +0 -2
- sqlspec/driver/parameters.py +4 -4
- sqlspec/extensions/aiosql/adapter.py +4 -4
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/plugin.py +3 -0
- sqlspec/loader.py +1 -1
- sqlspec/migrations/__init__.py +23 -0
- sqlspec/migrations/base.py +390 -0
- sqlspec/migrations/commands.py +525 -0
- sqlspec/migrations/runner.py +215 -0
- sqlspec/migrations/tracker.py +153 -0
- sqlspec/migrations/utils.py +89 -0
- sqlspec/protocols.py +37 -3
- sqlspec/statement/builder/__init__.py +8 -8
- sqlspec/statement/builder/{column.py → _column.py} +82 -52
- sqlspec/statement/builder/{ddl.py → _ddl.py} +5 -5
- sqlspec/statement/builder/_ddl_utils.py +1 -1
- sqlspec/statement/builder/{delete.py → _delete.py} +1 -1
- sqlspec/statement/builder/{insert.py → _insert.py} +1 -1
- sqlspec/statement/builder/{merge.py → _merge.py} +1 -1
- sqlspec/statement/builder/_parsing_utils.py +5 -3
- sqlspec/statement/builder/{select.py → _select.py} +59 -61
- sqlspec/statement/builder/{update.py → _update.py} +2 -2
- sqlspec/statement/builder/mixins/__init__.py +24 -30
- sqlspec/statement/builder/mixins/{_set_ops.py → _cte_and_set_ops.py} +86 -2
- sqlspec/statement/builder/mixins/{_delete_from.py → _delete_operations.py} +2 -0
- sqlspec/statement/builder/mixins/{_insert_values.py → _insert_operations.py} +70 -1
- sqlspec/statement/builder/mixins/{_merge_clauses.py → _merge_operations.py} +2 -0
- sqlspec/statement/builder/mixins/_order_limit_operations.py +123 -0
- sqlspec/statement/builder/mixins/{_pivot.py → _pivot_operations.py} +71 -2
- sqlspec/statement/builder/mixins/_select_operations.py +612 -0
- sqlspec/statement/builder/mixins/{_update_set.py → _update_operations.py} +73 -2
- sqlspec/statement/builder/mixins/_where_clause.py +536 -0
- sqlspec/statement/cache.py +50 -0
- sqlspec/statement/filters.py +37 -8
- sqlspec/statement/parameters.py +143 -54
- sqlspec/statement/pipelines/__init__.py +1 -1
- sqlspec/statement/pipelines/context.py +4 -10
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +3 -3
- sqlspec/statement/pipelines/validators/_parameter_style.py +22 -22
- sqlspec/statement/pipelines/validators/_performance.py +1 -5
- sqlspec/statement/sql.py +246 -176
- sqlspec/utils/__init__.py +2 -1
- sqlspec/utils/statement_hashing.py +203 -0
- sqlspec/utils/type_guards.py +32 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.1.dist-info}/METADATA +1 -1
- sqlspec-0.14.1.dist-info/RECORD +145 -0
- sqlspec-0.14.1.dist-info/entry_points.txt +2 -0
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
- sqlspec/statement/builder/mixins/_case_builder.py +0 -91
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
- sqlspec/statement/builder/mixins/_from.py +0 -63
- sqlspec/statement/builder/mixins/_group_by.py +0 -118
- sqlspec/statement/builder/mixins/_having.py +0 -35
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
- sqlspec/statement/builder/mixins/_insert_into.py +0 -36
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_table.py +0 -29
- sqlspec/statement/builder/mixins/_where.py +0 -401
- sqlspec/statement/builder/mixins/_window_functions.py +0 -86
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- /sqlspec/statement/builder/{base.py → _base.py} +0 -0
- /sqlspec/statement/builder/mixins/{_join.py → _join_operations.py} +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.14.1.dist-info}/licenses/NOTICE +0 -0
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
|
-
|
|
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()
|
|
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()
|
|
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()
|
|
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()
|
|
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()
|
|
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
|
-
|
|
210
|
+
"""Execute statement multiple times with different parameters.
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
|
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
|
-
|
|
339
|
-
if not
|
|
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
|
|
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.
|
sqlspec/driver/_sync.py
CHANGED
|
@@ -205,17 +205,23 @@ class SyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
|
|
|
205
205
|
_config: "Optional[SQLConfig]" = None,
|
|
206
206
|
**kwargs: Any,
|
|
207
207
|
) -> "SQLResult[RowT]":
|
|
208
|
-
|
|
208
|
+
"""Execute statement multiple times with different parameters.
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
210
|
+
Now passes first parameter set through pipeline to enable
|
|
211
|
+
literal extraction and consistent parameter processing.
|
|
212
|
+
"""
|
|
213
|
+
filters, param_sequence = process_execute_many_parameters(parameters)
|
|
214
|
+
|
|
215
|
+
# Process first parameter set through pipeline for literal extraction
|
|
216
|
+
first_params = param_sequence[0] if param_sequence else None
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
# Build statement with first params to trigger pipeline processing
|
|
219
|
+
sql_statement = self._build_statement(
|
|
220
|
+
statement, first_params, *filters, _config=_config or self.config, **kwargs
|
|
221
|
+
)
|
|
217
222
|
|
|
218
|
-
|
|
223
|
+
# Mark as many with full sequence
|
|
224
|
+
sql_statement = sql_statement.as_many(param_sequence)
|
|
219
225
|
|
|
220
226
|
return self._execute_statement(statement=sql_statement, connection=self._connection(_connection), **kwargs)
|
|
221
227
|
|
|
@@ -226,12 +232,24 @@ class SyncDriverAdapterProtocol(CommonDriverAttributesMixin[ConnectionT, RowT],
|
|
|
226
232
|
*parameters: "Union[StatementParameters, StatementFilter]",
|
|
227
233
|
_connection: "Optional[ConnectionT]" = None,
|
|
228
234
|
_config: "Optional[SQLConfig]" = None,
|
|
235
|
+
_suppress_warnings: bool = False, # New parameter for migrations
|
|
229
236
|
**kwargs: Any,
|
|
230
237
|
) -> "SQLResult[RowT]":
|
|
238
|
+
"""Execute a multi-statement script.
|
|
239
|
+
|
|
240
|
+
By default, validates each statement and logs warnings for dangerous
|
|
241
|
+
operations. Use _suppress_warnings=True for migrations and admin scripts.
|
|
242
|
+
"""
|
|
231
243
|
script_config = _config or self.config
|
|
232
|
-
|
|
233
|
-
|
|
244
|
+
|
|
245
|
+
# Keep validation enabled by default
|
|
246
|
+
# Validators will log warnings for dangerous operations
|
|
234
247
|
|
|
235
248
|
sql_statement = self._build_statement(statement, *parameters, _config=script_config, **kwargs)
|
|
236
249
|
sql_statement = sql_statement.as_script()
|
|
250
|
+
|
|
251
|
+
# Pass suppress warnings flag to execution
|
|
252
|
+
if _suppress_warnings:
|
|
253
|
+
kwargs["_suppress_warnings"] = True
|
|
254
|
+
|
|
237
255
|
return self._execute_statement(statement=sql_statement, connection=self._connection(_connection), **kwargs)
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
"""Driver mixins for instrumentation, storage, and utilities."""
|
|
2
2
|
|
|
3
|
+
from sqlspec.driver.mixins._cache import AsyncAdapterCacheMixin, SyncAdapterCacheMixin
|
|
3
4
|
from sqlspec.driver.mixins._pipeline import AsyncPipelinedExecutionMixin, SyncPipelinedExecutionMixin
|
|
5
|
+
from sqlspec.driver.mixins._query_tools import AsyncQueryMixin, SyncQueryMixin
|
|
4
6
|
from sqlspec.driver.mixins._result_utils import ToSchemaMixin
|
|
5
7
|
from sqlspec.driver.mixins._sql_translator import SQLTranslatorMixin
|
|
6
8
|
from sqlspec.driver.mixins._storage import AsyncStorageMixin, SyncStorageMixin
|
|
7
9
|
from sqlspec.driver.mixins._type_coercion import TypeCoercionMixin
|
|
8
10
|
|
|
9
11
|
__all__ = (
|
|
12
|
+
"AsyncAdapterCacheMixin",
|
|
10
13
|
"AsyncPipelinedExecutionMixin",
|
|
14
|
+
"AsyncQueryMixin",
|
|
11
15
|
"AsyncStorageMixin",
|
|
12
16
|
"SQLTranslatorMixin",
|
|
17
|
+
"SyncAdapterCacheMixin",
|
|
13
18
|
"SyncPipelinedExecutionMixin",
|
|
19
|
+
"SyncQueryMixin",
|
|
14
20
|
"SyncStorageMixin",
|
|
15
21
|
"ToSchemaMixin",
|
|
16
22
|
"TypeCoercionMixin",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Adapter-level caching mixin for compiled SQL and prepared statements."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
4
|
+
|
|
5
|
+
from sqlspec.statement.cache import SQLCache
|
|
6
|
+
from sqlspec.statement.parameters import ParameterStyle
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from sqlspec.statement.sql import SQL
|
|
10
|
+
|
|
11
|
+
__all__ = ("AsyncAdapterCacheMixin", "SyncAdapterCacheMixin")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SyncAdapterCacheMixin:
|
|
15
|
+
"""Mixin for adapter-level SQL compilation caching.
|
|
16
|
+
|
|
17
|
+
This mixin provides:
|
|
18
|
+
- Compiled SQL caching to avoid repeated compilation
|
|
19
|
+
- Parameter style conversion caching
|
|
20
|
+
- Prepared statement name management (for supported databases)
|
|
21
|
+
|
|
22
|
+
Integrates transparently with existing adapter execution flow.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
26
|
+
"""Initialize adapter with caching support."""
|
|
27
|
+
super().__init__(*args, **kwargs)
|
|
28
|
+
|
|
29
|
+
# Get cache configuration from config or use defaults
|
|
30
|
+
config = getattr(self, "config", None)
|
|
31
|
+
cache_size = getattr(config, "adapter_cache_size", 500) if config else 500
|
|
32
|
+
enable_cache = getattr(config, "enable_adapter_cache", True) if config else True
|
|
33
|
+
|
|
34
|
+
# Initialize caches
|
|
35
|
+
self._compiled_cache: Optional[SQLCache] = SQLCache(max_size=cache_size) if enable_cache else None
|
|
36
|
+
self._prepared_statements: dict[str, str] = {}
|
|
37
|
+
self._prepared_counter = 0
|
|
38
|
+
|
|
39
|
+
def _get_compiled_sql(self, statement: "SQL", target_style: ParameterStyle) -> tuple[str, Any]:
|
|
40
|
+
"""Get compiled SQL with caching.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
statement: SQL statement to compile
|
|
44
|
+
target_style: Target parameter style for compilation
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of (compiled_sql, parameters)
|
|
48
|
+
"""
|
|
49
|
+
if self._compiled_cache is None:
|
|
50
|
+
# Caching disabled
|
|
51
|
+
return statement.compile(placeholder_style=target_style)
|
|
52
|
+
|
|
53
|
+
# Generate cache key
|
|
54
|
+
cache_key = self._adapter_cache_key(statement, target_style)
|
|
55
|
+
|
|
56
|
+
# Check cache
|
|
57
|
+
cached = self._compiled_cache.get(cache_key)
|
|
58
|
+
if cached is not None:
|
|
59
|
+
return cached # type: ignore[no-any-return]
|
|
60
|
+
|
|
61
|
+
# Compile and cache
|
|
62
|
+
result = statement.compile(placeholder_style=target_style)
|
|
63
|
+
self._compiled_cache.set(cache_key, result)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
def _adapter_cache_key(self, statement: "SQL", style: ParameterStyle) -> str:
|
|
67
|
+
"""Generate adapter-specific cache key.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
statement: SQL statement
|
|
71
|
+
style: Parameter style
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Cache key string
|
|
75
|
+
"""
|
|
76
|
+
# Use statement's internal cache key which includes SQL hash, params, and dialect
|
|
77
|
+
base_key = statement._cache_key()
|
|
78
|
+
# Add adapter-specific context
|
|
79
|
+
return f"{self.__class__.__name__}:{style.value}:{base_key}"
|
|
80
|
+
|
|
81
|
+
def _get_or_create_prepared_statement_name(self, sql_hash: str) -> str:
|
|
82
|
+
"""Get or create a prepared statement name for the given SQL.
|
|
83
|
+
|
|
84
|
+
Used by PostgreSQL and other databases that support prepared statements.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
sql_hash: Hash of the SQL statement
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Prepared statement name
|
|
91
|
+
"""
|
|
92
|
+
if sql_hash in self._prepared_statements:
|
|
93
|
+
return self._prepared_statements[sql_hash]
|
|
94
|
+
|
|
95
|
+
# Create new prepared statement name
|
|
96
|
+
self._prepared_counter += 1
|
|
97
|
+
stmt_name = f"sqlspec_ps_{self._prepared_counter}"
|
|
98
|
+
self._prepared_statements[sql_hash] = stmt_name
|
|
99
|
+
return stmt_name
|
|
100
|
+
|
|
101
|
+
def _clear_adapter_cache(self) -> None:
|
|
102
|
+
"""Clear all adapter-level caches."""
|
|
103
|
+
if self._compiled_cache is not None:
|
|
104
|
+
self._compiled_cache.clear()
|
|
105
|
+
self._prepared_statements.clear()
|
|
106
|
+
self._prepared_counter = 0
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class AsyncAdapterCacheMixin(SyncAdapterCacheMixin):
|
|
110
|
+
"""Async version of AdapterCacheMixin.
|
|
111
|
+
|
|
112
|
+
Identical to AdapterCacheMixin since caching operations are synchronous.
|
|
113
|
+
Provided for naming consistency with async adapters.
|
|
114
|
+
"""
|