sqlspec 0.14.1__py3-none-any.whl → 0.16.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.
- sqlspec/__init__.py +50 -25
- sqlspec/__main__.py +1 -1
- sqlspec/__metadata__.py +1 -3
- sqlspec/_serialization.py +1 -2
- sqlspec/_sql.py +480 -121
- sqlspec/_typing.py +278 -142
- sqlspec/adapters/adbc/__init__.py +4 -3
- sqlspec/adapters/adbc/_types.py +12 -0
- sqlspec/adapters/adbc/config.py +115 -260
- sqlspec/adapters/adbc/driver.py +462 -367
- sqlspec/adapters/aiosqlite/__init__.py +18 -3
- sqlspec/adapters/aiosqlite/_types.py +13 -0
- sqlspec/adapters/aiosqlite/config.py +199 -129
- sqlspec/adapters/aiosqlite/driver.py +230 -269
- sqlspec/adapters/asyncmy/__init__.py +18 -3
- sqlspec/adapters/asyncmy/_types.py +12 -0
- sqlspec/adapters/asyncmy/config.py +80 -168
- sqlspec/adapters/asyncmy/driver.py +260 -225
- sqlspec/adapters/asyncpg/__init__.py +19 -4
- sqlspec/adapters/asyncpg/_types.py +17 -0
- sqlspec/adapters/asyncpg/config.py +82 -181
- sqlspec/adapters/asyncpg/driver.py +285 -383
- sqlspec/adapters/bigquery/__init__.py +17 -3
- sqlspec/adapters/bigquery/_types.py +12 -0
- sqlspec/adapters/bigquery/config.py +191 -258
- sqlspec/adapters/bigquery/driver.py +474 -646
- sqlspec/adapters/duckdb/__init__.py +14 -3
- sqlspec/adapters/duckdb/_types.py +12 -0
- sqlspec/adapters/duckdb/config.py +415 -351
- sqlspec/adapters/duckdb/driver.py +343 -413
- sqlspec/adapters/oracledb/__init__.py +19 -5
- sqlspec/adapters/oracledb/_types.py +14 -0
- sqlspec/adapters/oracledb/config.py +123 -379
- sqlspec/adapters/oracledb/driver.py +507 -560
- sqlspec/adapters/psqlpy/__init__.py +13 -3
- sqlspec/adapters/psqlpy/_types.py +11 -0
- sqlspec/adapters/psqlpy/config.py +93 -254
- sqlspec/adapters/psqlpy/driver.py +505 -234
- sqlspec/adapters/psycopg/__init__.py +19 -5
- sqlspec/adapters/psycopg/_types.py +17 -0
- sqlspec/adapters/psycopg/config.py +143 -403
- sqlspec/adapters/psycopg/driver.py +706 -872
- sqlspec/adapters/sqlite/__init__.py +14 -3
- sqlspec/adapters/sqlite/_types.py +11 -0
- sqlspec/adapters/sqlite/config.py +202 -118
- sqlspec/adapters/sqlite/driver.py +264 -303
- sqlspec/base.py +105 -9
- sqlspec/{statement/builder → builder}/__init__.py +12 -14
- sqlspec/{statement/builder → builder}/_base.py +120 -55
- sqlspec/{statement/builder → builder}/_column.py +17 -6
- sqlspec/{statement/builder → builder}/_ddl.py +46 -79
- sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
- sqlspec/{statement/builder → builder}/_delete.py +6 -25
- sqlspec/{statement/builder → builder}/_insert.py +18 -65
- sqlspec/builder/_merge.py +56 -0
- sqlspec/{statement/builder → builder}/_parsing_utils.py +8 -11
- sqlspec/{statement/builder → builder}/_select.py +11 -56
- sqlspec/{statement/builder → builder}/_update.py +12 -18
- sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
- sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
- sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +34 -18
- sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
- sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +19 -9
- sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
- sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
- sqlspec/{statement/builder → builder}/mixins/_select_operations.py +25 -38
- sqlspec/{statement/builder → builder}/mixins/_update_operations.py +15 -16
- sqlspec/{statement/builder → builder}/mixins/_where_clause.py +210 -137
- sqlspec/cli.py +4 -5
- sqlspec/config.py +180 -133
- sqlspec/core/__init__.py +63 -0
- sqlspec/core/cache.py +873 -0
- sqlspec/core/compiler.py +396 -0
- sqlspec/core/filters.py +830 -0
- sqlspec/core/hashing.py +310 -0
- sqlspec/core/parameters.py +1209 -0
- sqlspec/core/result.py +664 -0
- sqlspec/{statement → core}/splitter.py +321 -191
- sqlspec/core/statement.py +666 -0
- sqlspec/driver/__init__.py +7 -10
- sqlspec/driver/_async.py +387 -176
- sqlspec/driver/_common.py +527 -289
- sqlspec/driver/_sync.py +390 -172
- sqlspec/driver/mixins/__init__.py +2 -19
- sqlspec/driver/mixins/_result_tools.py +164 -0
- sqlspec/driver/mixins/_sql_translator.py +6 -3
- sqlspec/exceptions.py +5 -252
- sqlspec/extensions/aiosql/adapter.py +93 -96
- sqlspec/extensions/litestar/cli.py +1 -1
- sqlspec/extensions/litestar/config.py +0 -1
- sqlspec/extensions/litestar/handlers.py +15 -26
- sqlspec/extensions/litestar/plugin.py +18 -16
- sqlspec/extensions/litestar/providers.py +17 -52
- sqlspec/loader.py +424 -105
- sqlspec/migrations/__init__.py +12 -0
- sqlspec/migrations/base.py +92 -68
- sqlspec/migrations/commands.py +24 -106
- sqlspec/migrations/loaders.py +402 -0
- sqlspec/migrations/runner.py +49 -51
- sqlspec/migrations/tracker.py +31 -44
- sqlspec/migrations/utils.py +64 -24
- sqlspec/protocols.py +7 -183
- 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 -3
- 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 +443 -232
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/METADATA +7 -2
- sqlspec-0.16.0.dist-info/RECORD +134 -0
- sqlspec/adapters/adbc/transformers.py +0 -108
- sqlspec/driver/connection.py +0 -207
- sqlspec/driver/mixins/_cache.py +0 -114
- sqlspec/driver/mixins/_csv_writer.py +0 -91
- sqlspec/driver/mixins/_pipeline.py +0 -508
- sqlspec/driver/mixins/_query_tools.py +0 -796
- sqlspec/driver/mixins/_result_utils.py +0 -138
- sqlspec/driver/mixins/_storage.py +0 -912
- sqlspec/driver/mixins/_type_coercion.py +0 -128
- sqlspec/driver/parameters.py +0 -138
- sqlspec/statement/__init__.py +0 -21
- sqlspec/statement/builder/_merge.py +0 -95
- sqlspec/statement/cache.py +0 -50
- sqlspec/statement/filters.py +0 -625
- sqlspec/statement/parameters.py +0 -956
- 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 -109
- 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 -714
- sqlspec/statement/pipelines/validators/_security.py +0 -967
- sqlspec/statement/result.py +0 -435
- sqlspec/statement/sql.py +0 -1774
- sqlspec/utils/cached_property.py +0 -25
- sqlspec/utils/statement_hashing.py +0 -203
- sqlspec-0.14.1.dist-info/RECORD +0 -145
- /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/entry_points.txt +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.14.1.dist-info → sqlspec-0.16.0.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,12 @@
|
|
|
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
8
|
from dataclasses import dataclass, field
|
|
10
|
-
from typing import TYPE_CHECKING, Any,
|
|
9
|
+
from typing import TYPE_CHECKING, Any, NoReturn, Optional, Union, cast
|
|
11
10
|
|
|
12
11
|
import sqlglot
|
|
13
12
|
from sqlglot import Dialect, exp
|
|
@@ -16,14 +15,16 @@ from sqlglot.errors import ParseError as SQLGlotParseError
|
|
|
16
15
|
from sqlglot.optimizer import optimize
|
|
17
16
|
from typing_extensions import Self
|
|
18
17
|
|
|
18
|
+
from sqlspec.core.cache import CacheKey, get_cache_config, get_default_cache
|
|
19
|
+
from sqlspec.core.hashing import hash_optimized_expression
|
|
20
|
+
from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
|
|
21
|
+
from sqlspec.core.statement import SQL, StatementConfig
|
|
19
22
|
from sqlspec.exceptions import SQLBuilderError
|
|
20
|
-
from sqlspec.statement.sql import SQL, SQLConfig
|
|
21
|
-
from sqlspec.typing import RowT
|
|
22
23
|
from sqlspec.utils.logging import get_logger
|
|
23
24
|
from sqlspec.utils.type_guards import has_sql_method, has_with_method
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
26
|
-
from sqlspec.
|
|
27
|
+
from sqlspec.core.result import SQLResult
|
|
27
28
|
|
|
28
29
|
__all__ = ("QueryBuilder", "SafeQuery")
|
|
29
30
|
|
|
@@ -40,18 +41,11 @@ class SafeQuery:
|
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
@dataclass
|
|
43
|
-
class QueryBuilder(ABC
|
|
44
|
+
class QueryBuilder(ABC):
|
|
44
45
|
"""Abstract base class for SQL query builders with SQLGlot optimization.
|
|
45
46
|
|
|
46
47
|
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
|
|
48
|
+
query construction, and query optimization using SQLGlot.
|
|
55
49
|
"""
|
|
56
50
|
|
|
57
51
|
dialect: DialectType = field(default=None)
|
|
@@ -68,7 +62,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
68
62
|
def __post_init__(self) -> None:
|
|
69
63
|
self._expression = self._create_base_expression()
|
|
70
64
|
if not self._expression:
|
|
71
|
-
# This path should be unreachable if _raise_sql_builder_error has NoReturn
|
|
72
65
|
self._raise_sql_builder_error(
|
|
73
66
|
"QueryBuilder._create_base_expression must return a valid sqlglot expression."
|
|
74
67
|
)
|
|
@@ -77,17 +70,13 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
77
70
|
def _create_base_expression(self) -> exp.Expression:
|
|
78
71
|
"""Create the base sqlglot expression for the specific query type.
|
|
79
72
|
|
|
80
|
-
Examples:
|
|
81
|
-
For a SELECT query, this would return `exp.Select()`.
|
|
82
|
-
For an INSERT query, this would return `exp.Insert()`.
|
|
83
|
-
|
|
84
73
|
Returns:
|
|
85
|
-
|
|
74
|
+
A new sqlglot expression appropriate for the query type.
|
|
86
75
|
"""
|
|
87
76
|
|
|
88
77
|
@property
|
|
89
78
|
@abstractmethod
|
|
90
|
-
def _expected_result_type(self) -> "type[SQLResult
|
|
79
|
+
def _expected_result_type(self) -> "type[SQLResult]":
|
|
91
80
|
"""The expected result type for the query being built.
|
|
92
81
|
|
|
93
82
|
Returns:
|
|
@@ -119,7 +108,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
119
108
|
"""
|
|
120
109
|
self._parameter_counter += 1
|
|
121
110
|
|
|
122
|
-
# Use context-aware naming if provided
|
|
123
111
|
param_name = f"{context}_param_{self._parameter_counter}" if context else f"param_{self._parameter_counter}"
|
|
124
112
|
|
|
125
113
|
self._parameters[param_name] = value
|
|
@@ -141,10 +129,8 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
141
129
|
|
|
142
130
|
def replacer(node: exp.Expression) -> exp.Expression:
|
|
143
131
|
if isinstance(node, exp.Literal):
|
|
144
|
-
|
|
145
|
-
if node.this in (True, False, None):
|
|
132
|
+
if node.this in {True, False, None}:
|
|
146
133
|
return node
|
|
147
|
-
# Convert other literals to parameters
|
|
148
134
|
param_name = self._add_parameter(node.this, context="where")
|
|
149
135
|
return exp.Placeholder(this=param_name)
|
|
150
136
|
return node
|
|
@@ -195,7 +181,47 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
195
181
|
return name
|
|
196
182
|
i += 1
|
|
197
183
|
|
|
198
|
-
def
|
|
184
|
+
def _generate_builder_cache_key(self, config: "Optional[StatementConfig]" = None) -> str:
|
|
185
|
+
"""Generate cache key based on builder state and configuration.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
config: Optional SQL configuration that affects the generated SQL
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
A unique cache key representing the builder state and configuration
|
|
192
|
+
"""
|
|
193
|
+
import hashlib
|
|
194
|
+
|
|
195
|
+
state_components = [
|
|
196
|
+
f"expression:{self._expression.sql() if self._expression else 'None'}",
|
|
197
|
+
f"parameters:{sorted(self._parameters.items())}",
|
|
198
|
+
f"ctes:{sorted(self._with_ctes.keys())}",
|
|
199
|
+
f"dialect:{self.dialect_name or 'default'}",
|
|
200
|
+
f"schema:{self.schema}",
|
|
201
|
+
f"optimization:{self.enable_optimization}",
|
|
202
|
+
f"optimize_joins:{self.optimize_joins}",
|
|
203
|
+
f"optimize_predicates:{self.optimize_predicates}",
|
|
204
|
+
f"simplify_expressions:{self.simplify_expressions}",
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
if config:
|
|
208
|
+
config_components = [
|
|
209
|
+
f"config_dialect:{config.dialect or 'default'}",
|
|
210
|
+
f"enable_parsing:{config.enable_parsing}",
|
|
211
|
+
f"enable_validation:{config.enable_validation}",
|
|
212
|
+
f"enable_transformations:{config.enable_transformations}",
|
|
213
|
+
f"enable_analysis:{config.enable_analysis}",
|
|
214
|
+
f"enable_caching:{config.enable_caching}",
|
|
215
|
+
f"param_style:{config.parameter_config.default_parameter_style.value}",
|
|
216
|
+
]
|
|
217
|
+
state_components.extend(config_components)
|
|
218
|
+
|
|
219
|
+
state_string = "|".join(state_components)
|
|
220
|
+
cache_key = hashlib.sha256(state_string.encode()).hexdigest()[:16]
|
|
221
|
+
|
|
222
|
+
return f"builder:{cache_key}"
|
|
223
|
+
|
|
224
|
+
def with_cte(self: Self, alias: str, query: "Union[QueryBuilder, exp.Select, str]") -> Self:
|
|
199
225
|
"""Adds a Common Table Expression (CTE) to the query.
|
|
200
226
|
|
|
201
227
|
Args:
|
|
@@ -219,7 +245,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
219
245
|
self._raise_sql_builder_error(msg)
|
|
220
246
|
cte_select_expression = query._expression.copy()
|
|
221
247
|
for p_name, p_value in query.parameters.items():
|
|
222
|
-
# Try to preserve original parameter name, only rename if collision
|
|
223
248
|
unique_name = self._generate_unique_parameter_name(p_name)
|
|
224
249
|
self.add_parameter(p_value, unique_name)
|
|
225
250
|
|
|
@@ -229,7 +254,6 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
229
254
|
if not isinstance(parsed_expression, exp.Select):
|
|
230
255
|
msg = f"CTE query string must parse to a SELECT statement, got {type(parsed_expression).__name__}."
|
|
231
256
|
self._raise_sql_builder_error(msg)
|
|
232
|
-
# parsed_expression is now known to be exp.Select
|
|
233
257
|
cte_select_expression = parsed_expression
|
|
234
258
|
except SQLGlotParseError as e:
|
|
235
259
|
self._raise_sql_builder_error(f"Failed to parse CTE query string: {e!s}", e)
|
|
@@ -241,7 +265,7 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
241
265
|
else:
|
|
242
266
|
msg = f"Invalid query type for CTE: {type(query).__name__}"
|
|
243
267
|
self._raise_sql_builder_error(msg)
|
|
244
|
-
return self
|
|
268
|
+
return self
|
|
245
269
|
|
|
246
270
|
self._with_ctes[alias] = exp.CTE(this=cte_select_expression, alias=exp.to_table(alias))
|
|
247
271
|
return self
|
|
@@ -255,22 +279,22 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
255
279
|
if self._expression is None:
|
|
256
280
|
self._raise_sql_builder_error("QueryBuilder expression not initialized.")
|
|
257
281
|
|
|
258
|
-
final_expression = self._expression.copy()
|
|
259
|
-
|
|
260
282
|
if self._with_ctes:
|
|
283
|
+
final_expression = self._expression.copy()
|
|
261
284
|
if has_with_method(final_expression):
|
|
262
|
-
# Type checker now knows final_expression has with_ method
|
|
263
285
|
for alias, cte_node in self._with_ctes.items():
|
|
264
286
|
final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
|
|
265
287
|
elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
|
|
266
288
|
final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
|
|
289
|
+
else:
|
|
290
|
+
final_expression = self._expression.copy() if self.enable_optimization else self._expression
|
|
267
291
|
|
|
268
292
|
if self.enable_optimization and isinstance(final_expression, exp.Expression):
|
|
269
293
|
final_expression = self._optimize_expression(final_expression)
|
|
270
294
|
|
|
271
295
|
try:
|
|
272
296
|
if has_sql_method(final_expression):
|
|
273
|
-
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
|
|
297
|
+
sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
|
|
274
298
|
else:
|
|
275
299
|
sql_string = str(final_expression)
|
|
276
300
|
except Exception as e:
|
|
@@ -281,7 +305,7 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
281
305
|
return SafeQuery(sql=sql_string, parameters=self._parameters.copy(), dialect=self.dialect)
|
|
282
306
|
|
|
283
307
|
def _optimize_expression(self, expression: exp.Expression) -> exp.Expression:
|
|
284
|
-
"""Apply SQLGlot optimizations to the expression.
|
|
308
|
+
"""Apply SQLGlot optimizations to the expression with caching.
|
|
285
309
|
|
|
286
310
|
Args:
|
|
287
311
|
expression: The expression to optimize
|
|
@@ -292,24 +316,63 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
292
316
|
if not self.enable_optimization:
|
|
293
317
|
return expression
|
|
294
318
|
|
|
319
|
+
optimizer_settings = {
|
|
320
|
+
"optimize_joins": self.optimize_joins,
|
|
321
|
+
"pushdown_predicates": self.optimize_predicates,
|
|
322
|
+
"simplify_expressions": self.simplify_expressions,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
dialect_name = self.dialect_name or "default"
|
|
326
|
+
cache_key = hash_optimized_expression(
|
|
327
|
+
expression, dialect=dialect_name, schema=self.schema, optimizer_settings=optimizer_settings
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
cache_key_obj = CacheKey((cache_key,))
|
|
331
|
+
unified_cache = get_default_cache()
|
|
332
|
+
cached_optimized = unified_cache.get(cache_key_obj)
|
|
333
|
+
if cached_optimized:
|
|
334
|
+
return cast("exp.Expression", cached_optimized)
|
|
335
|
+
|
|
295
336
|
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
|
-
},
|
|
337
|
+
optimized = optimize(
|
|
338
|
+
expression.copy(), schema=self.schema, dialect=self.dialect_name, optimizer_settings=optimizer_settings
|
|
306
339
|
)
|
|
340
|
+
|
|
341
|
+
unified_cache.put(cache_key_obj, optimized.copy())
|
|
342
|
+
|
|
307
343
|
except Exception:
|
|
308
|
-
# Continue with unoptimized query on failure
|
|
309
344
|
return expression
|
|
345
|
+
else:
|
|
346
|
+
return optimized
|
|
310
347
|
|
|
311
|
-
def to_statement(self, config: "Optional[
|
|
312
|
-
"""Converts the built query into a SQL statement object.
|
|
348
|
+
def to_statement(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
349
|
+
"""Converts the built query into a SQL statement object with caching.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
config: Optional SQL configuration.
|
|
353
|
+
|
|
354
|
+
Returns:
|
|
355
|
+
SQL: A SQL statement object.
|
|
356
|
+
"""
|
|
357
|
+
cache_config = get_cache_config()
|
|
358
|
+
if not cache_config.compiled_cache_enabled:
|
|
359
|
+
return self._to_statement_without_cache(config)
|
|
360
|
+
|
|
361
|
+
cache_key_str = self._generate_builder_cache_key(config)
|
|
362
|
+
cache_key = CacheKey((cache_key_str,))
|
|
363
|
+
|
|
364
|
+
unified_cache = get_default_cache()
|
|
365
|
+
cached_sql = unified_cache.get(cache_key)
|
|
366
|
+
if cached_sql is not None:
|
|
367
|
+
return cast("SQL", cached_sql)
|
|
368
|
+
|
|
369
|
+
sql_statement = self._to_statement_without_cache(config)
|
|
370
|
+
unified_cache.put(cache_key, sql_statement)
|
|
371
|
+
|
|
372
|
+
return sql_statement
|
|
373
|
+
|
|
374
|
+
def _to_statement_without_cache(self, config: "Optional[StatementConfig]" = None) -> "SQL":
|
|
375
|
+
"""Internal method to create SQL statement without caching.
|
|
313
376
|
|
|
314
377
|
Args:
|
|
315
378
|
config: Optional SQL configuration.
|
|
@@ -321,7 +384,7 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
321
384
|
|
|
322
385
|
if isinstance(safe_query.parameters, dict):
|
|
323
386
|
kwargs = safe_query.parameters
|
|
324
|
-
parameters = None
|
|
387
|
+
parameters: Optional[tuple] = None
|
|
325
388
|
else:
|
|
326
389
|
kwargs = None
|
|
327
390
|
parameters = (
|
|
@@ -333,16 +396,18 @@ class QueryBuilder(ABC, Generic[RowT]):
|
|
|
333
396
|
)
|
|
334
397
|
|
|
335
398
|
if config is None:
|
|
336
|
-
from sqlspec.statement
|
|
399
|
+
from sqlspec.core.statement import StatementConfig
|
|
337
400
|
|
|
338
|
-
|
|
401
|
+
parameter_config = ParameterStyleConfig(
|
|
402
|
+
default_parameter_style=ParameterStyle.QMARK, supported_parameter_styles={ParameterStyle.QMARK}
|
|
403
|
+
)
|
|
404
|
+
config = StatementConfig(parameter_config=parameter_config, dialect=safe_query.dialect)
|
|
339
405
|
|
|
340
|
-
# SQL expects parameters as variadic args, not as a keyword
|
|
341
406
|
if kwargs:
|
|
342
|
-
return SQL(safe_query.sql,
|
|
407
|
+
return SQL(safe_query.sql, statement_config=config, **kwargs)
|
|
343
408
|
if parameters:
|
|
344
|
-
return SQL(safe_query.sql, *parameters,
|
|
345
|
-
return SQL(safe_query.sql,
|
|
409
|
+
return SQL(safe_query.sql, *parameters, statement_config=config)
|
|
410
|
+
return SQL(safe_query.sql, statement_config=config)
|
|
346
411
|
|
|
347
412
|
def __str__(self) -> str:
|
|
348
413
|
"""Return the SQL string representation of the query.
|
|
@@ -17,6 +17,8 @@ __all__ = ("Column", "ColumnExpression", "FunctionColumn")
|
|
|
17
17
|
class ColumnExpression:
|
|
18
18
|
"""Base class for column expressions that can be combined with operators."""
|
|
19
19
|
|
|
20
|
+
__slots__ = ("_expression",)
|
|
21
|
+
|
|
20
22
|
def __init__(self, expression: exp.Expression) -> None:
|
|
21
23
|
self._expression = expression
|
|
22
24
|
|
|
@@ -54,11 +56,12 @@ class ColumnExpression:
|
|
|
54
56
|
class Column:
|
|
55
57
|
"""Represents a database column with Python operator support."""
|
|
56
58
|
|
|
59
|
+
__slots__ = ("_expression", "name", "table")
|
|
60
|
+
|
|
57
61
|
def __init__(self, name: str, table: Optional[str] = None) -> None:
|
|
58
62
|
self.name = name
|
|
59
63
|
self.table = table
|
|
60
64
|
|
|
61
|
-
# Create SQLGlot column expression
|
|
62
65
|
if table:
|
|
63
66
|
self._expression = exp.Column(this=exp.Identifier(this=name), table=exp.Identifier(this=table))
|
|
64
67
|
else:
|
|
@@ -174,7 +177,7 @@ class Column:
|
|
|
174
177
|
"""SQL ROUND() function."""
|
|
175
178
|
if decimals == 0:
|
|
176
179
|
return FunctionColumn(exp.Round(this=self._expression))
|
|
177
|
-
return FunctionColumn(exp.Round(this=self._expression, expression=exp.
|
|
180
|
+
return FunctionColumn(exp.Round(this=self._expression, expression=exp.convert(decimals)))
|
|
178
181
|
|
|
179
182
|
def floor(self) -> "FunctionColumn":
|
|
180
183
|
"""SQL FLOOR() function."""
|
|
@@ -186,9 +189,9 @@ class Column:
|
|
|
186
189
|
|
|
187
190
|
def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
|
|
188
191
|
"""SQL SUBSTRING() function."""
|
|
189
|
-
args = [exp.
|
|
192
|
+
args = [exp.convert(start)]
|
|
190
193
|
if length is not None:
|
|
191
|
-
args.append(exp.
|
|
194
|
+
args.append(exp.convert(length))
|
|
192
195
|
return FunctionColumn(exp.Substring(this=self._expression, expressions=args))
|
|
193
196
|
|
|
194
197
|
def coalesce(self, *values: Any) -> "FunctionColumn":
|
|
@@ -234,6 +237,14 @@ class Column:
|
|
|
234
237
|
"""Create an aliased column expression."""
|
|
235
238
|
return exp.Alias(this=self._expression, alias=alias_name)
|
|
236
239
|
|
|
240
|
+
def asc(self) -> exp.Ordered:
|
|
241
|
+
"""Create an ASC ordering expression."""
|
|
242
|
+
return exp.Ordered(this=self._expression, desc=False)
|
|
243
|
+
|
|
244
|
+
def desc(self) -> exp.Ordered:
|
|
245
|
+
"""Create a DESC ordering expression."""
|
|
246
|
+
return exp.Ordered(this=self._expression, desc=True)
|
|
247
|
+
|
|
237
248
|
def __repr__(self) -> str:
|
|
238
249
|
if self.table:
|
|
239
250
|
return f"Column<{self.table}.{self.name}>"
|
|
@@ -247,6 +258,8 @@ class Column:
|
|
|
247
258
|
class FunctionColumn:
|
|
248
259
|
"""Represents the result of a SQL function call on a column."""
|
|
249
260
|
|
|
261
|
+
__slots__ = ("_expression",)
|
|
262
|
+
|
|
250
263
|
def __init__(self, expression: exp.Expression) -> None:
|
|
251
264
|
self._expression = expression
|
|
252
265
|
|
|
@@ -306,8 +319,6 @@ class FunctionColumn:
|
|
|
306
319
|
"""Create an aliased function expression."""
|
|
307
320
|
return exp.Alias(this=self._expression, alias=alias_name)
|
|
308
321
|
|
|
309
|
-
# Add other operators as needed...
|
|
310
|
-
|
|
311
322
|
def __hash__(self) -> int:
|
|
312
323
|
"""Hash based on the SQL expression."""
|
|
313
324
|
return hash(self._expression.sql() if has_sql_method(self._expression) else str(self._expression))
|