sqlspec 0.13.1__py3-none-any.whl → 0.16.2__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 +71 -8
- sqlspec/__main__.py +12 -0
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +930 -136
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +116 -285
- sqlspec/adapters/adbc/driver.py +462 -340
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +202 -150
- sqlspec/adapters/aiosqlite/driver.py +226 -247
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -199
- sqlspec/adapters/asyncmy/driver.py +257 -215
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +81 -214
- sqlspec/adapters/asyncpg/driver.py +284 -359
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -299
- sqlspec/adapters/bigquery/driver.py +474 -634
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +414 -397
- sqlspec/adapters/duckdb/driver.py +342 -393
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -458
- sqlspec/adapters/oracledb/driver.py +505 -531
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -307
- sqlspec/adapters/psqlpy/driver.py +504 -213
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -472
- sqlspec/adapters/psycopg/driver.py +704 -825
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +208 -142
- sqlspec/adapters/sqlite/driver.py +263 -278
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
- sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
- sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
- sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
- sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
- sqlspec/builder/_insert.py +421 -0
- sqlspec/builder/_merge.py +71 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
- sqlspec/builder/_select.py +170 -0
- sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
- sqlspec/builder/mixins/__init__.py +55 -0
- sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
- sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
- sqlspec/builder/mixins/_insert_operations.py +244 -0
- sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
- sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
- sqlspec/builder/mixins/_order_limit_operations.py +135 -0
- sqlspec/builder/mixins/_pivot_operations.py +153 -0
- sqlspec/builder/mixins/_select_operations.py +604 -0
- sqlspec/builder/mixins/_update_operations.py +202 -0
- sqlspec/builder/mixins/_where_clause.py +644 -0
- sqlspec/cli.py +247 -0
- sqlspec/config.py +183 -138
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +871 -0
- sqlspec/core/compiler.py +417 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1237 -0
- sqlspec/core/result.py +677 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +676 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +422 -163
- sqlspec/driver/_common.py +545 -287
- sqlspec/driver/_sync.py +426 -160
- sqlspec/driver/mixins/__init__.py +2 -13
- sqlspec/driver/mixins/_result_tools.py +193 -0
- sqlspec/driver/mixins/_sql_translator.py +65 -14
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/__init__.py +2 -1
- sqlspec/extensions/litestar/cli.py +48 -0
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +21 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +423 -104
- sqlspec/migrations/__init__.py +35 -0
- sqlspec/migrations/base.py +414 -0
- sqlspec/migrations/commands.py +443 -0
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +213 -0
- sqlspec/migrations/tracker.py +140 -0
- sqlspec/migrations/utils.py +129 -0
- sqlspec/protocols.py +51 -186
- sqlspec/storage/__init__.py +1 -1
- sqlspec/storage/backends/base.py +37 -40
- sqlspec/storage/backends/fsspec.py +136 -112
- sqlspec/storage/backends/obstore.py +138 -160
- sqlspec/storage/capabilities.py +5 -4
- sqlspec/storage/registry.py +57 -106
- sqlspec/typing.py +136 -115
- sqlspec/utils/__init__.py +2 -2
- sqlspec/utils/correlation.py +0 -3
- sqlspec/utils/deprecation.py +6 -6
- sqlspec/utils/fixtures.py +6 -6
- sqlspec/utils/logging.py +0 -2
- sqlspec/utils/module_loader.py +7 -12
- sqlspec/utils/singleton.py +0 -1
- sqlspec/utils/sync_tools.py +17 -38
- sqlspec/utils/text.py +12 -51
- sqlspec/utils/type_guards.py +482 -235
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
- sqlspec-0.16.2.dist-info/RECORD +134 -0
- sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -512
- sqlspec/driver/mixins/_result_utils.py +0 -140
- sqlspec/driver/mixins/_storage.py +0 -926
- sqlspec/driver/mixins/_type_coercion.py +0 -130
- sqlspec/driver/parameters.py +0 -138
- sqlspec/service/__init__.py +0 -4
- sqlspec/service/_util.py +0 -147
- sqlspec/service/base.py +0 -1131
- sqlspec/service/pagination.py +0 -26
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/insert.py +0 -288
- sqlspec/statement/builder/merge.py +0 -95
- sqlspec/statement/builder/mixins/__init__.py +0 -65
- 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/_insert_values.py +0 -67
- sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
- sqlspec/statement/builder/mixins/_order_by.py +0 -46
- sqlspec/statement/builder/mixins/_pivot.py +0 -79
- sqlspec/statement/builder/mixins/_returning.py +0 -37
- sqlspec/statement/builder/mixins/_select_columns.py +0 -61
- sqlspec/statement/builder/mixins/_set_ops.py +0 -122
- sqlspec/statement/builder/mixins/_unpivot.py +0 -77
- sqlspec/statement/builder/mixins/_update_from.py +0 -55
- sqlspec/statement/builder/mixins/_update_set.py +0 -94
- 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/builder/select.py +0 -221
- sqlspec/statement/filters.py +0 -596
- sqlspec/statement/parameter_manager.py +0 -220
- sqlspec/statement/parameters.py +0 -867
- sqlspec/statement/pipelines/__init__.py +0 -210
- sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
- sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
- sqlspec/statement/pipelines/context.py +0 -115
- sqlspec/statement/pipelines/transformers/__init__.py +0 -7
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
- sqlspec/statement/pipelines/validators/__init__.py +0 -23
- sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
- sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
- sqlspec/statement/pipelines/validators/_performance.py +0 -718
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1704
- sqlspec/statement/sql_compiler.py +0 -140
- sqlspec/utils/cached_property.py +0 -25
- sqlspec-0.13.1.dist-info/RECORD +0 -150
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/NOTICE +0 -0
sqlspec/base.py
CHANGED
|
@@ -13,6 +13,15 @@ from sqlspec.config import (
|
|
|
13
13
|
SyncConfigT,
|
|
14
14
|
SyncDatabaseConfig,
|
|
15
15
|
)
|
|
16
|
+
from sqlspec.core.cache import (
|
|
17
|
+
CacheConfig,
|
|
18
|
+
CacheStatsAggregate,
|
|
19
|
+
get_cache_config,
|
|
20
|
+
get_cache_stats,
|
|
21
|
+
log_cache_stats,
|
|
22
|
+
reset_cache_stats,
|
|
23
|
+
update_cache_config,
|
|
24
|
+
)
|
|
16
25
|
from sqlspec.utils.logging import get_logger
|
|
17
26
|
|
|
18
27
|
if TYPE_CHECKING:
|
|
@@ -27,18 +36,19 @@ logger = get_logger()
|
|
|
27
36
|
|
|
28
37
|
|
|
29
38
|
class SQLSpec:
|
|
30
|
-
"""
|
|
39
|
+
"""Configuration manager and registry for database connections and pools."""
|
|
31
40
|
|
|
32
|
-
__slots__ = ("_configs",)
|
|
41
|
+
__slots__ = ("_cleanup_tasks", "_configs", "_instance_cache_config")
|
|
33
42
|
|
|
34
43
|
def __init__(self) -> None:
|
|
35
44
|
self._configs: dict[Any, DatabaseConfigProtocol[Any, Any, Any]] = {}
|
|
36
45
|
atexit.register(self._cleanup_pools)
|
|
46
|
+
self._instance_cache_config: Optional[CacheConfig] = None
|
|
47
|
+
self._cleanup_tasks: list[asyncio.Task[None]] = []
|
|
37
48
|
|
|
38
49
|
@staticmethod
|
|
39
50
|
def _get_config_name(obj: Any) -> str:
|
|
40
51
|
"""Get display name for configuration object."""
|
|
41
|
-
# Try to get __name__ attribute if it exists, otherwise use str()
|
|
42
52
|
return getattr(obj, "__name__", str(obj))
|
|
43
53
|
|
|
44
54
|
def _cleanup_pools(self) -> None:
|
|
@@ -54,11 +64,11 @@ class SQLSpec:
|
|
|
54
64
|
try:
|
|
55
65
|
loop = asyncio.get_running_loop()
|
|
56
66
|
if loop.is_running():
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
task = asyncio.create_task(cast("Coroutine[Any, Any, None]", close_pool_awaitable))
|
|
68
|
+
self._cleanup_tasks.append(task)
|
|
59
69
|
else:
|
|
60
70
|
asyncio.run(cast("Coroutine[Any, Any, None]", close_pool_awaitable))
|
|
61
|
-
except RuntimeError:
|
|
71
|
+
except RuntimeError:
|
|
62
72
|
asyncio.run(cast("Coroutine[Any, Any, None]", close_pool_awaitable))
|
|
63
73
|
else:
|
|
64
74
|
config.close_pool()
|
|
@@ -66,6 +76,14 @@ class SQLSpec:
|
|
|
66
76
|
except Exception as e:
|
|
67
77
|
logger.warning("Failed to clean up pool for config %s: %s", config_type.__name__, e)
|
|
68
78
|
|
|
79
|
+
if self._cleanup_tasks:
|
|
80
|
+
try:
|
|
81
|
+
loop = asyncio.get_running_loop()
|
|
82
|
+
if loop.is_running():
|
|
83
|
+
asyncio.gather(*self._cleanup_tasks, return_exceptions=True)
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
69
87
|
self._configs.clear()
|
|
70
88
|
logger.info("Pool cleanup completed. Cleaned %d pools.", cleaned_count)
|
|
71
89
|
|
|
@@ -233,14 +251,13 @@ class SQLSpec:
|
|
|
233
251
|
async def _create_driver_async() -> "DriverT":
|
|
234
252
|
resolved_connection = await connection_obj # pyright: ignore
|
|
235
253
|
return cast( # pyright: ignore
|
|
236
|
-
"DriverT",
|
|
237
|
-
config.driver_type(connection=resolved_connection, default_row_type=config.default_row_type),
|
|
254
|
+
"DriverT", config.driver_type(connection=resolved_connection)
|
|
238
255
|
)
|
|
239
256
|
|
|
240
257
|
return _create_driver_async()
|
|
241
258
|
|
|
242
259
|
return cast( # pyright: ignore
|
|
243
|
-
"DriverT", config.driver_type(connection=connection_obj
|
|
260
|
+
"DriverT", config.driver_type(connection=connection_obj)
|
|
244
261
|
)
|
|
245
262
|
|
|
246
263
|
@overload
|
|
@@ -473,3 +490,82 @@ class SQLSpec:
|
|
|
473
490
|
|
|
474
491
|
logger.debug("Config %s does not support connection pooling - nothing to close", config_name)
|
|
475
492
|
return None
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def get_cache_config() -> CacheConfig:
|
|
496
|
+
"""Get the current global cache configuration.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
The current cache configuration.
|
|
500
|
+
"""
|
|
501
|
+
return get_cache_config()
|
|
502
|
+
|
|
503
|
+
@staticmethod
|
|
504
|
+
def update_cache_config(config: CacheConfig) -> None:
|
|
505
|
+
"""Update the global cache configuration.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
config: The new cache configuration to apply.
|
|
509
|
+
"""
|
|
510
|
+
update_cache_config(config)
|
|
511
|
+
|
|
512
|
+
@staticmethod
|
|
513
|
+
def get_cache_stats() -> CacheStatsAggregate:
|
|
514
|
+
"""Get current cache statistics.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
Cache statistics object with detailed metrics.
|
|
518
|
+
"""
|
|
519
|
+
return get_cache_stats()
|
|
520
|
+
|
|
521
|
+
@staticmethod
|
|
522
|
+
def reset_cache_stats() -> None:
|
|
523
|
+
"""Reset all cache statistics to zero."""
|
|
524
|
+
reset_cache_stats()
|
|
525
|
+
|
|
526
|
+
@staticmethod
|
|
527
|
+
def log_cache_stats() -> None:
|
|
528
|
+
"""Log current cache statistics using the configured logger."""
|
|
529
|
+
log_cache_stats()
|
|
530
|
+
|
|
531
|
+
@staticmethod
|
|
532
|
+
def configure_cache(
|
|
533
|
+
*,
|
|
534
|
+
sql_cache_size: Optional[int] = None,
|
|
535
|
+
fragment_cache_size: Optional[int] = None,
|
|
536
|
+
optimized_cache_size: Optional[int] = None,
|
|
537
|
+
sql_cache_enabled: Optional[bool] = None,
|
|
538
|
+
fragment_cache_enabled: Optional[bool] = None,
|
|
539
|
+
optimized_cache_enabled: Optional[bool] = None,
|
|
540
|
+
) -> None:
|
|
541
|
+
"""Update cache configuration with partial values.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
sql_cache_size: Size of the SQL statement cache
|
|
545
|
+
fragment_cache_size: Size of the AST fragment cache
|
|
546
|
+
optimized_cache_size: Size of the optimized expression cache
|
|
547
|
+
sql_cache_enabled: Enable/disable SQL cache
|
|
548
|
+
fragment_cache_enabled: Enable/disable fragment cache
|
|
549
|
+
optimized_cache_enabled: Enable/disable optimized cache
|
|
550
|
+
"""
|
|
551
|
+
current_config = get_cache_config()
|
|
552
|
+
update_cache_config(
|
|
553
|
+
CacheConfig(
|
|
554
|
+
sql_cache_size=sql_cache_size if sql_cache_size is not None else current_config.sql_cache_size,
|
|
555
|
+
fragment_cache_size=fragment_cache_size
|
|
556
|
+
if fragment_cache_size is not None
|
|
557
|
+
else current_config.fragment_cache_size,
|
|
558
|
+
optimized_cache_size=optimized_cache_size
|
|
559
|
+
if optimized_cache_size is not None
|
|
560
|
+
else current_config.optimized_cache_size,
|
|
561
|
+
sql_cache_enabled=sql_cache_enabled
|
|
562
|
+
if sql_cache_enabled is not None
|
|
563
|
+
else current_config.sql_cache_enabled,
|
|
564
|
+
fragment_cache_enabled=fragment_cache_enabled
|
|
565
|
+
if fragment_cache_enabled is not None
|
|
566
|
+
else current_config.fragment_cache_enabled,
|
|
567
|
+
optimized_cache_enabled=optimized_cache_enabled
|
|
568
|
+
if optimized_cache_enabled is not None
|
|
569
|
+
else current_config.optimized_cache_enabled,
|
|
570
|
+
)
|
|
571
|
+
)
|
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This package provides fluent interfaces for building SQL queries with automatic
|
|
4
4
|
parameter binding and validation.
|
|
5
|
-
|
|
6
|
-
# SelectBuilder is now generic and supports as_schema for type-safe schema integration.
|
|
7
5
|
"""
|
|
8
6
|
|
|
9
|
-
from sqlspec.
|
|
10
|
-
from sqlspec.
|
|
11
|
-
from sqlspec.
|
|
12
|
-
from sqlspec.statement.builder.ddl import (
|
|
7
|
+
from sqlspec.builder._base import QueryBuilder, SafeQuery
|
|
8
|
+
from sqlspec.builder._column import Column, ColumnExpression, FunctionColumn
|
|
9
|
+
from sqlspec.builder._ddl import (
|
|
13
10
|
AlterTable,
|
|
14
11
|
CommentOn,
|
|
15
12
|
CreateIndex,
|
|
@@ -24,14 +21,15 @@ from sqlspec.statement.builder.ddl import (
|
|
|
24
21
|
DropTable,
|
|
25
22
|
DropView,
|
|
26
23
|
RenameTable,
|
|
27
|
-
|
|
24
|
+
Truncate,
|
|
28
25
|
)
|
|
29
|
-
from sqlspec.
|
|
30
|
-
from sqlspec.
|
|
31
|
-
from sqlspec.
|
|
32
|
-
from sqlspec.
|
|
33
|
-
from sqlspec.
|
|
34
|
-
from sqlspec.
|
|
26
|
+
from sqlspec.builder._delete import Delete
|
|
27
|
+
from sqlspec.builder._insert import Insert
|
|
28
|
+
from sqlspec.builder._merge import Merge
|
|
29
|
+
from sqlspec.builder._select import Select
|
|
30
|
+
from sqlspec.builder._update import Update
|
|
31
|
+
from sqlspec.builder.mixins import WhereClauseMixin
|
|
32
|
+
from sqlspec.exceptions import SQLBuilderError
|
|
35
33
|
|
|
36
34
|
__all__ = (
|
|
37
35
|
"AlterTable",
|
|
@@ -58,7 +56,7 @@ __all__ = (
|
|
|
58
56
|
"SQLBuilderError",
|
|
59
57
|
"SafeQuery",
|
|
60
58
|
"Select",
|
|
61
|
-
"
|
|
59
|
+
"Truncate",
|
|
62
60
|
"Update",
|
|
63
61
|
"WhereClauseMixin",
|
|
64
62
|
)
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
"""Safe SQL query builder with validation and parameter binding.
|
|
2
2
|
|
|
3
3
|
This module provides a fluent interface for building SQL queries safely,
|
|
4
|
-
with automatic parameter binding and validation.
|
|
5
|
-
advanced builder patterns and optimization capabilities.
|
|
4
|
+
with automatic parameter binding and validation.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from abc import ABC, abstractmethod
|
|
9
|
-
from
|
|
10
|
-
from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union, cast
|
|
8
|
+
from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
|
|
11
9
|
|
|
12
10
|
import sqlglot
|
|
13
11
|
from sqlglot import Dialect, exp
|
|
@@ -16,59 +14,81 @@ from sqlglot.errors import ParseError as SQLGlotParseError
|
|
|
16
14
|
from sqlglot.optimizer import optimize
|
|
17
15
|
from typing_extensions import Self
|
|
18
16
|
|
|
17
|
+
from sqlspec.core.cache import CacheKey, get_cache_config, get_default_cache
|
|
18
|
+
from sqlspec.core.hashing import hash_optimized_expression
|
|
19
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
|
|
20
|
+
from sqlspec.core.statement import SQL, StatementConfig
|
|
19
21
|
from sqlspec.exceptions import SQLBuilderError
|
|
20
|
-
from sqlspec.statement.sql import SQL, SQLConfig
|
|
21
|
-
from sqlspec.typing import RowT
|
|
22
22
|
from sqlspec.utils.logging import get_logger
|
|
23
23
|
from sqlspec.utils.type_guards import has_sql_method, has_with_method
|
|
24
24
|
|
|
25
25
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.
|
|
26
|
+
from sqlspec.core.result import SQLResult
|
|
27
27
|
|
|
28
28
|
__all__ = ("QueryBuilder", "SafeQuery")
|
|
29
29
|
|
|
30
30
|
logger = get_logger(__name__)
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
@dataclass(frozen=True)
|
|
34
33
|
class SafeQuery:
|
|
35
34
|
"""A safely constructed SQL query with bound parameters."""
|
|
36
35
|
|
|
37
|
-
sql
|
|
38
|
-
parameters: dict[str, Any] = field(default_factory=dict)
|
|
39
|
-
dialect: DialectType = field(default=None)
|
|
36
|
+
__slots__ = ("dialect", "parameters", "sql")
|
|
40
37
|
|
|
38
|
+
def __init__(
|
|
39
|
+
self, sql: str, parameters: Optional[dict[str, Any]] = None, dialect: Optional[DialectType] = None
|
|
40
|
+
) -> None:
|
|
41
|
+
self.sql = sql
|
|
42
|
+
self.parameters = parameters if parameters is not None else {}
|
|
43
|
+
self.dialect = dialect
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
class QueryBuilder(ABC
|
|
45
|
+
|
|
46
|
+
class QueryBuilder(ABC):
|
|
44
47
|
"""Abstract base class for SQL query builders with SQLGlot optimization.
|
|
45
48
|
|
|
46
49
|
Provides common functionality for dialect handling, parameter management,
|
|
47
|
-
query construction, and
|
|
48
|
-
advanced capabilities.
|
|
49
|
-
|
|
50
|
-
New features:
|
|
51
|
-
- Automatic query optimization (join reordering, predicate pushdown)
|
|
52
|
-
- Query complexity analysis
|
|
53
|
-
- Smart parameter naming based on context
|
|
54
|
-
- Expression caching for performance
|
|
50
|
+
query construction, and query optimization using SQLGlot.
|
|
55
51
|
"""
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
__slots__ = (
|
|
54
|
+
"_expression",
|
|
55
|
+
"_parameter_counter",
|
|
56
|
+
"_parameters",
|
|
57
|
+
"_with_ctes",
|
|
58
|
+
"dialect",
|
|
59
|
+
"enable_optimization",
|
|
60
|
+
"optimize_joins",
|
|
61
|
+
"optimize_predicates",
|
|
62
|
+
"schema",
|
|
63
|
+
"simplify_expressions",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
dialect: Optional[DialectType] = None,
|
|
69
|
+
schema: Optional[dict[str, dict[str, str]]] = None,
|
|
70
|
+
enable_optimization: bool = True,
|
|
71
|
+
optimize_joins: bool = True,
|
|
72
|
+
optimize_predicates: bool = True,
|
|
73
|
+
simplify_expressions: bool = True,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.dialect = dialect
|
|
76
|
+
self.schema = schema
|
|
77
|
+
self.enable_optimization = enable_optimization
|
|
78
|
+
self.optimize_joins = optimize_joins
|
|
79
|
+
self.optimize_predicates = optimize_predicates
|
|
80
|
+
self.simplify_expressions = simplify_expressions
|
|
81
|
+
|
|
82
|
+
# Initialize mutable attributes
|
|
83
|
+
self._expression: Optional[exp.Expression] = None
|
|
84
|
+
self._parameters: dict[str, Any] = {}
|
|
85
|
+
self._parameter_counter: int = 0
|
|
86
|
+
self._with_ctes: dict[str, exp.CTE] = {}
|
|
87
|
+
|
|
88
|
+
def _initialize_expression(self) -> None:
|
|
89
|
+
"""Initialize the base expression. Called after __init__."""
|
|
69
90
|
self._expression = self._create_base_expression()
|
|
70
91
|
if not self._expression:
|
|
71
|
-
# This path should be unreachable if _raise_sql_builder_error has NoReturn
|
|
72
92
|
self._raise_sql_builder_error(
|
|
73
93
|
"QueryBuilder._create_base_expression must return a valid sqlglot expression."
|
|
74
94
|
)
|
|
@@ -77,17 +97,13 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
77
97
|
def _create_base_expression(self) -> exp.Expression:
|
|
78
98
|
"""Create the base sqlglot expression for the specific query type.
|
|
79
99
|
|
|
80
|
-
Examples:
|
|
81
|
-
For a SELECT query, this would return `exp.Select()`.
|
|
82
|
-
For an INSERT query, this would return `exp.Insert()`.
|
|
83
|
-
|
|
84
100
|
Returns:
|
|
85
|
-
|
|
101
|
+
A new sqlglot expression appropriate for the query type.
|
|
86
102
|
"""
|
|
87
103
|
|
|
88
104
|
@property
|
|
89
105
|
@abstractmethod
|
|
90
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
106
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
91
107
|
"""The expected result type for the query being built.
|
|
92
108
|
|
|
93
109
|
Returns:
|
|
@@ -119,7 +135,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
119
135
|
"""
|
|
120
136
|
self._parameter_counter += 1
|
|
121
137
|
|
|
122
|
-
# Use context-aware naming if provided
|
|
123
138
|
param_name = f"{context}_param_{self._parameter_counter}" if context else f"param_{self._parameter_counter}"
|
|
124
139
|
|
|
125
140
|
self._parameters[param_name] = value
|
|
@@ -141,15 +156,13 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
141
156
|
|
|
142
157
|
def replacer(node: exp.Expression) -> exp.Expression:
|
|
143
158
|
if isinstance(node, exp.Literal):
|
|
144
|
-
|
|
145
|
-
if node.this in (True, False, None):
|
|
159
|
+
if node.this in {True, False, None}:
|
|
146
160
|
return node
|
|
147
|
-
# Convert other literals to parameters
|
|
148
161
|
param_name = self._add_parameter(node.this, context="where")
|
|
149
162
|
return exp.Placeholder(this=param_name)
|
|
150
163
|
return node
|
|
151
164
|
|
|
152
|
-
return expression.transform(replacer, copy=
|
|
165
|
+
return expression.transform(replacer, copy=False)
|
|
153
166
|
|
|
154
167
|
def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
|
|
155
168
|
"""Explicitly adds a parameter to the query.
|
|
@@ -168,13 +181,13 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
168
181
|
if name:
|
|
169
182
|
if name in self._parameters:
|
|
170
183
|
self._raise_sql_builder_error(f"Parameter name '{name}' already exists.")
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
self._parameter_counter += 1
|
|
174
|
-
param_name_to_use = f"param_{self._parameter_counter}"
|
|
184
|
+
self._parameters[name] = value
|
|
185
|
+
return self, name
|
|
175
186
|
|
|
176
|
-
self.
|
|
177
|
-
|
|
187
|
+
self._parameter_counter += 1
|
|
188
|
+
param_name = f"param_{self._parameter_counter}"
|
|
189
|
+
self._parameters[param_name] = value
|
|
190
|
+
return self, param_name
|
|
178
191
|
|
|
179
192
|
def _generate_unique_parameter_name(self, base_name: str) -> str:
|
|
180
193
|
"""Generate unique parameter name when collision occurs.
|
|
@@ -188,14 +201,58 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
188
201
|
if base_name not in self._parameters:
|
|
189
202
|
return base_name
|
|
190
203
|
|
|
191
|
-
i
|
|
192
|
-
while True:
|
|
204
|
+
for i in range(1, 1000): # Reasonable upper bound to prevent infinite loops
|
|
193
205
|
name = f"{base_name}_{i}"
|
|
194
206
|
if name not in self._parameters:
|
|
195
207
|
return name
|
|
196
|
-
i += 1
|
|
197
208
|
|
|
198
|
-
|
|
209
|
+
# Fallback for edge case
|
|
210
|
+
import uuid
|
|
211
|
+
|
|
212
|
+
return f"{base_name}_{uuid.uuid4().hex[:8]}"
|
|
213
|
+
|
|
214
|
+
def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
|
|
215
|
+
"""Generate cache key based on builder state and configuration.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
config: Optional SQL configuration that affects the generated SQL
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
A unique cache key representing the builder state and configuration
|
|
222
|
+
"""
|
|
223
|
+
import hashlib
|
|
224
|
+
|
|
225
|
+
dialect_name: str = self.dialect_name or "default"
|
|
226
|
+
expr_sql: str = self._expression.sql() if self._expression else "None"
|
|
227
|
+
|
|
228
|
+
state_parts = [
|
|
229
|
+
f"expression:{expr_sql}",
|
|
230
|
+
f"parameters:{sorted(self._parameters.items())}",
|
|
231
|
+
f"ctes:{sorted(self._with_ctes.keys())}",
|
|
232
|
+
f"dialect:{dialect_name}",
|
|
233
|
+
f"schema:{self.schema}",
|
|
234
|
+
f"optimization:{self.enable_optimization}",
|
|
235
|
+
f"optimize_joins:{self.optimize_joins}",
|
|
236
|
+
f"optimize_predicates:{self.optimize_predicates}",
|
|
237
|
+
f"simplify_expressions:{self.simplify_expressions}",
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
if config:
|
|
241
|
+
config_parts = [
|
|
242
|
+
f"config_dialect:{config.dialect or 'default'}",
|
|
243
|
+
f"enable_parsing:{config.enable_parsing}",
|
|
244
|
+
f"enable_validation:{config.enable_validation}",
|
|
245
|
+
f"enable_transformations:{config.enable_transformations}",
|
|
246
|
+
f"enable_analysis:{config.enable_analysis}",
|
|
247
|
+
f"enable_caching:{config.enable_caching}",
|
|
248
|
+
f"param_style:{config.parameter_config.default_parameter_style.value}",
|
|
249
|
+
]
|
|
250
|
+
state_parts.extend(config_parts)
|
|
251
|
+
|
|
252
|
+
state_string = "|".join(state_parts)
|
|
253
|
+
return f"builder:{hashlib.sha256(state_string.encode()).hexdigest()[:16]}"
|
|
254
|
+
|
|
255
|
+
def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
|
|
199
256
|
"""Adds a Common Table Expression (CTE) to the query.
|
|
200
257
|
|
|
201
258
|
Args:
|
|
@@ -217,9 +274,8 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
217
274
|
if not isinstance(query._expression, exp.Select):
|
|
218
275
|
msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
|
|
219
276
|
self._raise_sql_builder_error(msg)
|
|
220
|
-
cte_select_expression = query._expression
|
|
277
|
+
cte_select_expression = query._expression
|
|
221
278
|
for p_name, p_value in query.parameters.items():
|
|
222
|
-
# Try to preserve original parameter name, only rename if collision
|
|
223
279
|
unique_name = self._generate_unique_parameter_name(p_name)
|
|
224
280
|
self.add_parameter(p_value, unique_name)
|
|
225
281
|
|
|
@@ -229,7 +285,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
229
285
|
if not isinstance(parsed_expression, exp.Select):
|
|
230
286
|
msg = f"CTE query string must parse to a SELECT statement, got {type(parsed_expression).__name__}."
|
|
231
287
|
self._raise_sql_builder_error(msg)
|
|
232
|
-
# parsed_expression is now known to be exp.Select
|
|
233
288
|
cte_select_expression = parsed_expression
|
|
234
289
|
except SQLGlotParseError as e:
|
|
235
290
|
self._raise_sql_builder_error(f"Failed to parse CTE query string: {e!s}", e)
|
|
@@ -237,11 +292,11 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
237
292
|
msg = f"An unexpected error occurred while parsing CTE query string: {e!s}"
|
|
238
293
|
self._raise_sql_builder_error(msg, e)
|
|
239
294
|
elif isinstance(query, exp.Select):
|
|
240
|
-
cte_select_expression = query
|
|
295
|
+
cte_select_expression = query
|
|
241
296
|
else:
|
|
242
297
|
msg = f"Invalid query type for CTE: {type(query).__name__}"
|
|
243
298
|
self._raise_sql_builder_error(msg)
|
|
244
|
-
return self
|
|
299
|
+
return self
|
|
245
300
|
|
|
246
301
|
self._with_ctes[alias] = exp.CTE(this=cte_select_expression, alias=exp.to_table(alias))
|
|
247
302
|
return self
|
|
@@ -255,22 +310,22 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
255
310
|
if self._expression is None:
|
|
256
311
|
self._raise_sql_builder_error("QueryBuilder expression not initialized.")
|
|
257
312
|
|
|
258
|
-
final_expression = self._expression.copy()
|
|
259
|
-
|
|
260
313
|
if self._with_ctes:
|
|
314
|
+
final_expression = self._expression
|
|
261
315
|
if has_with_method(final_expression):
|
|
262
|
-
# Type checker now knows final_expression has with_ method
|
|
263
316
|
for alias, cte_node in self._with_ctes.items():
|
|
264
317
|
final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
|
|
265
318
|
elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
|
|
266
319
|
final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
|
|
320
|
+
else:
|
|
321
|
+
final_expression = self._expression
|
|
267
322
|
|
|
268
323
|
if self.enable_optimization and isinstance(final_expression, exp.Expression):
|
|
269
324
|
final_expression = self._optimize_expression(final_expression)
|
|
270
325
|
|
|
271
326
|
try:
|
|
272
327
|
if has_sql_method(final_expression):
|
|
273
|
-
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
|
|
328
|
+
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
|
|
274
329
|
else:
|
|
275
330
|
sql_string = str(final_expression)
|
|
276
331
|
except Exception as e:
|
|
@@ -281,7 +336,7 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
281
336
|
return SafeQuery(sql=sql_string, parameters=self._parameters.copy(), dialect=self.dialect)
|
|
282
337
|
|
|
283
338
|
def _optimize_expression(self, expression: exp.Expression) -> exp.Expression:
|
|
284
|
-
"""Apply SQLGlot optimizations to the expression.
|
|
339
|
+
"""Apply SQLGlot optimizations to the expression with caching.
|
|
285
340
|
|
|
286
341
|
Args:
|
|
287
342
|
expression: The expression to optimize
|
|
@@ -292,24 +347,63 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
292
347
|
if not self.enable_optimization:
|
|
293
348
|
return expression
|
|
294
349
|
|
|
350
|
+
optimizer_settings = {
|
|
351
|
+
"optimize_joins": self.optimize_joins,
|
|
352
|
+
"pushdown_predicates": self.optimize_predicates,
|
|
353
|
+
"simplify_expressions": self.simplify_expressions,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
dialect_name = self.dialect_name or "default"
|
|
357
|
+
cache_key = hash_optimized_expression(
|
|
358
|
+
expression, dialect=dialect_name, schema=self.schema, optimizer_settings=optimizer_settings
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
cache_key_obj = CacheKey((cache_key,))
|
|
362
|
+
unified_cache = get_default_cache()
|
|
363
|
+
cached_optimized = unified_cache.get(cache_key_obj)
|
|
364
|
+
if cached_optimized:
|
|
365
|
+
return cast("exp.Expression", cached_optimized)
|
|
366
|
+
|
|
295
367
|
try:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
expression.copy(),
|
|
299
|
-
schema=self.schema,
|
|
300
|
-
dialect=self.dialect_name,
|
|
301
|
-
optimizer_settings={
|
|
302
|
-
"optimize_joins": self.optimize_joins,
|
|
303
|
-
"pushdown_predicates": self.optimize_predicates,
|
|
304
|
-
"simplify_expressions": self.simplify_expressions,
|
|
305
|
-
},
|
|
368
|
+
optimized = optimize(
|
|
369
|
+
expression, schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
|
|
306
370
|
)
|
|
371
|
+
|
|
372
|
+
unified_cache.put(cache_key_obj, optimized)
|
|
373
|
+
|
|
307
374
|
except Exception:
|
|
308
|
-
# Continue with unoptimized query on failure
|
|
309
375
|
return expression
|
|
376
|
+
else:
|
|
377
|
+
return optimized
|
|
310
378
|
|
|
311
|
-
def to_statement(self, config: "Optional[
|
|
312
|
-
"""Converts the built query into a SQL statement object.
|
|
379
|
+
def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
380
|
+
"""Converts the built query into a SQL statement object with caching.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
config: Optional SQL configuration.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
SQL: A SQL statement object.
|
|
387
|
+
"""
|
|
388
|
+
cache_config = get_cache_config()
|
|
389
|
+
if not cache_config.compiled_cache_enabled:
|
|
390
|
+
return self._to_statement_without_cache(config)
|
|
391
|
+
|
|
392
|
+
cache_key_str = self._generate_builder_cache_key(config)
|
|
393
|
+
cache_key = CacheKey((cache_key_str,))
|
|
394
|
+
|
|
395
|
+
unified_cache = get_default_cache()
|
|
396
|
+
cached_sql = unified_cache.get(cache_key)
|
|
397
|
+
if cached_sql is not None:
|
|
398
|
+
return cast("SQL", cached_sql)
|
|
399
|
+
|
|
400
|
+
sql_statement = self._to_statement_without_cache(config)
|
|
401
|
+
unified_cache.put(cache_key, sql_statement)
|
|
402
|
+
|
|
403
|
+
return sql_statement
|
|
404
|
+
|
|
405
|
+
def _to_statement_without_cache(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
406
|
+
"""Internal method to create SQL statement without caching.
|
|
313
407
|
|
|
314
408
|
Args:
|
|
315
409
|
config: Optional SQL configuration.
|
|
@@ -321,7 +415,7 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
321
415
|
|
|
322
416
|
if isinstance(safe_query.parameters, dict):
|
|
323
417
|
kwargs = safe_query.parameters
|
|
324
|
-
parameters = None
|
|
418
|
+
parameters: Optional[tuple] = None
|
|
325
419
|
else:
|
|
326
420
|
kwargs = None
|
|
327
421
|
parameters = (
|
|
@@ -333,16 +427,18 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
333
427
|
)
|
|
334
428
|
|
|
335
429
|
if config is None:
|
|
336
|
-
from sqlspec.statement
|
|
430
|
+
from sqlspec.core.statement import StatementConfig
|
|
337
431
|
|
|
338
|
-
|
|
432
|
+
parameter_config = ParameterStyleConfig(
|
|
433
|
+
default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK}
|
|
434
|
+
)
|
|
435
|
+
config = StatementConfig(parameter_config=parameter_config, dialect=safe_query.dialect)
|
|
339
436
|
|
|
340
|
-
# SQL expects parameters as variadic args, not as a keyword
|
|
341
437
|
if kwargs:
|
|
342
|
-
return SQL(safe_query.sql,
|
|
438
|
+
return SQL(safe_query.sql, statement_config=config, **kwargs)
|
|
343
439
|
if parameters:
|
|
344
|
-
return SQL(safe_query.sql, *parameters,
|
|
345
|
-
return SQL(safe_query.sql,
|
|
440
|
+
return SQL(safe_query.sql, *parameters, statement_config=config)
|
|
441
|
+
return SQL(safe_query.sql, statement_config=config)
|
|
346
442
|
|
|
347
443
|
def __str__(self) -> str:
|
|
348
444
|
"""Return the SQL string representation of the query.
|
|
@@ -365,8 +461,10 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
365
461
|
return self.dialect.__name__.lower()
|
|
366
462
|
if isinstance(self.dialect, Dialect):
|
|
367
463
|
return type(self.dialect).__name__.lower()
|
|
368
|
-
|
|
464
|
+
try:
|
|
369
465
|
return self.dialect.__name__.lower()
|
|
466
|
+
except AttributeError:
|
|
467
|
+
pass
|
|
370
468
|
return None
|
|
371
469
|
|
|
372
470
|
@property
|