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.

Files changed (185) hide show
  1. sqlspec/__init__.py +71 -8
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +930 -136
  6. sqlspec/_typing.py +278 -142
  7. sqlspec/adapters/adbc/__init__.py +4 -3
  8. sqlspec/adapters/adbc/_types.py +12 -0
  9. sqlspec/adapters/adbc/config.py +116 -285
  10. sqlspec/adapters/adbc/driver.py +462 -340
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +202 -150
  14. sqlspec/adapters/aiosqlite/driver.py +226 -247
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -199
  18. sqlspec/adapters/asyncmy/driver.py +257 -215
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +81 -214
  22. sqlspec/adapters/asyncpg/driver.py +284 -359
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -299
  26. sqlspec/adapters/bigquery/driver.py +474 -634
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +414 -397
  30. sqlspec/adapters/duckdb/driver.py +342 -393
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -458
  34. sqlspec/adapters/oracledb/driver.py +505 -531
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -307
  38. sqlspec/adapters/psqlpy/driver.py +504 -213
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -472
  42. sqlspec/adapters/psycopg/driver.py +704 -825
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +208 -142
  46. sqlspec/adapters/sqlite/driver.py +263 -278
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder/base.py → builder/_base.py} +184 -86
  50. sqlspec/{statement/builder/column.py → builder/_column.py} +97 -60
  51. sqlspec/{statement/builder/ddl.py → builder/_ddl.py} +61 -131
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +4 -10
  53. sqlspec/{statement/builder/delete.py → builder/_delete.py} +10 -30
  54. sqlspec/builder/_insert.py +421 -0
  55. sqlspec/builder/_merge.py +71 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +49 -26
  57. sqlspec/builder/_select.py +170 -0
  58. sqlspec/{statement/builder/update.py → builder/_update.py} +16 -20
  59. sqlspec/builder/mixins/__init__.py +55 -0
  60. sqlspec/builder/mixins/_cte_and_set_ops.py +222 -0
  61. sqlspec/{statement/builder/mixins/_delete_from.py → builder/mixins/_delete_operations.py} +8 -1
  62. sqlspec/builder/mixins/_insert_operations.py +244 -0
  63. sqlspec/{statement/builder/mixins/_join.py → builder/mixins/_join_operations.py} +45 -13
  64. sqlspec/{statement/builder/mixins/_merge_clauses.py → builder/mixins/_merge_operations.py} +188 -30
  65. sqlspec/builder/mixins/_order_limit_operations.py +135 -0
  66. sqlspec/builder/mixins/_pivot_operations.py +153 -0
  67. sqlspec/builder/mixins/_select_operations.py +604 -0
  68. sqlspec/builder/mixins/_update_operations.py +202 -0
  69. sqlspec/builder/mixins/_where_clause.py +644 -0
  70. sqlspec/cli.py +247 -0
  71. sqlspec/config.py +183 -138
  72. sqlspec/core/__init__.py +63 -0
  73. sqlspec/core/cache.py +871 -0
  74. sqlspec/core/compiler.py +417 -0
  75. sqlspec/core/filters.py +830 -0
  76. sqlspec/core/hashing.py +310 -0
  77. sqlspec/core/parameters.py +1237 -0
  78. sqlspec/core/result.py +677 -0
  79. sqlspec/{statement → core}/splitter.py +321 -191
  80. sqlspec/core/statement.py +676 -0
  81. sqlspec/driver/__init__.py +7 -10
  82. sqlspec/driver/_async.py +422 -163
  83. sqlspec/driver/_common.py +545 -287
  84. sqlspec/driver/_sync.py +426 -160
  85. sqlspec/driver/mixins/__init__.py +2 -13
  86. sqlspec/driver/mixins/_result_tools.py +193 -0
  87. sqlspec/driver/mixins/_sql_translator.py +65 -14
  88. sqlspec/exceptions.py +5 -252
  89. sqlspec/extensions/aiosql/adapter.py +93 -96
  90. sqlspec/extensions/litestar/__init__.py +2 -1
  91. sqlspec/extensions/litestar/cli.py +48 -0
  92. sqlspec/extensions/litestar/config.py +0 -1
  93. sqlspec/extensions/litestar/handlers.py +15 -26
  94. sqlspec/extensions/litestar/plugin.py +21 -16
  95. sqlspec/extensions/litestar/providers.py +17 -52
  96. sqlspec/loader.py +423 -104
  97. sqlspec/migrations/__init__.py +35 -0
  98. sqlspec/migrations/base.py +414 -0
  99. sqlspec/migrations/commands.py +443 -0
  100. sqlspec/migrations/loaders.py +402 -0
  101. sqlspec/migrations/runner.py +213 -0
  102. sqlspec/migrations/tracker.py +140 -0
  103. sqlspec/migrations/utils.py +129 -0
  104. sqlspec/protocols.py +51 -186
  105. sqlspec/storage/__init__.py +1 -1
  106. sqlspec/storage/backends/base.py +37 -40
  107. sqlspec/storage/backends/fsspec.py +136 -112
  108. sqlspec/storage/backends/obstore.py +138 -160
  109. sqlspec/storage/capabilities.py +5 -4
  110. sqlspec/storage/registry.py +57 -106
  111. sqlspec/typing.py +136 -115
  112. sqlspec/utils/__init__.py +2 -2
  113. sqlspec/utils/correlation.py +0 -3
  114. sqlspec/utils/deprecation.py +6 -6
  115. sqlspec/utils/fixtures.py +6 -6
  116. sqlspec/utils/logging.py +0 -2
  117. sqlspec/utils/module_loader.py +7 -12
  118. sqlspec/utils/singleton.py +0 -1
  119. sqlspec/utils/sync_tools.py +17 -38
  120. sqlspec/utils/text.py +12 -51
  121. sqlspec/utils/type_guards.py +482 -235
  122. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/METADATA +7 -2
  123. sqlspec-0.16.2.dist-info/RECORD +134 -0
  124. sqlspec-0.16.2.dist-info/entry_points.txt +2 -0
  125. sqlspec/driver/connection.py +0 -207
  126. sqlspec/driver/mixins/_csv_writer.py +0 -91
  127. sqlspec/driver/mixins/_pipeline.py +0 -512
  128. sqlspec/driver/mixins/_result_utils.py +0 -140
  129. sqlspec/driver/mixins/_storage.py +0 -926
  130. sqlspec/driver/mixins/_type_coercion.py +0 -130
  131. sqlspec/driver/parameters.py +0 -138
  132. sqlspec/service/__init__.py +0 -4
  133. sqlspec/service/_util.py +0 -147
  134. sqlspec/service/base.py +0 -1131
  135. sqlspec/service/pagination.py +0 -26
  136. sqlspec/statement/__init__.py +0 -21
  137. sqlspec/statement/builder/insert.py +0 -288
  138. sqlspec/statement/builder/merge.py +0 -95
  139. sqlspec/statement/builder/mixins/__init__.py +0 -65
  140. sqlspec/statement/builder/mixins/_aggregate_functions.py +0 -250
  141. sqlspec/statement/builder/mixins/_case_builder.py +0 -91
  142. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -90
  143. sqlspec/statement/builder/mixins/_from.py +0 -63
  144. sqlspec/statement/builder/mixins/_group_by.py +0 -118
  145. sqlspec/statement/builder/mixins/_having.py +0 -35
  146. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -47
  147. sqlspec/statement/builder/mixins/_insert_into.py +0 -36
  148. sqlspec/statement/builder/mixins/_insert_values.py +0 -67
  149. sqlspec/statement/builder/mixins/_limit_offset.py +0 -53
  150. sqlspec/statement/builder/mixins/_order_by.py +0 -46
  151. sqlspec/statement/builder/mixins/_pivot.py +0 -79
  152. sqlspec/statement/builder/mixins/_returning.py +0 -37
  153. sqlspec/statement/builder/mixins/_select_columns.py +0 -61
  154. sqlspec/statement/builder/mixins/_set_ops.py +0 -122
  155. sqlspec/statement/builder/mixins/_unpivot.py +0 -77
  156. sqlspec/statement/builder/mixins/_update_from.py +0 -55
  157. sqlspec/statement/builder/mixins/_update_set.py +0 -94
  158. sqlspec/statement/builder/mixins/_update_table.py +0 -29
  159. sqlspec/statement/builder/mixins/_where.py +0 -401
  160. sqlspec/statement/builder/mixins/_window_functions.py +0 -86
  161. sqlspec/statement/builder/select.py +0 -221
  162. sqlspec/statement/filters.py +0 -596
  163. sqlspec/statement/parameter_manager.py +0 -220
  164. sqlspec/statement/parameters.py +0 -867
  165. sqlspec/statement/pipelines/__init__.py +0 -210
  166. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  167. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  168. sqlspec/statement/pipelines/context.py +0 -115
  169. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  170. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  171. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  172. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  173. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  174. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  175. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  176. sqlspec/statement/pipelines/validators/_performance.py +0 -718
  177. sqlspec/statement/pipelines/validators/_security.py +0 -967
  178. sqlspec/statement/result.py +0 -435
  179. sqlspec/statement/sql.py +0 -1704
  180. sqlspec/statement/sql_compiler.py +0 -140
  181. sqlspec/utils/cached_property.py +0 -25
  182. sqlspec-0.13.1.dist-info/RECORD +0 -150
  183. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/WHEEL +0 -0
  184. {sqlspec-0.13.1.dist-info → sqlspec-0.16.2.dist-info}/licenses/LICENSE +0 -0
  185. {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
- """Type-safe configuration manager and registry for database connections and pools."""
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
- _task = asyncio.ensure_future(close_pool_awaitable, loop=loop) # noqa: RUF006
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: # No running event loop
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, default_row_type=config.default_row_type)
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.exceptions import SQLBuilderError
10
- from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
11
- from sqlspec.statement.builder.column import Column, ColumnExpression, FunctionColumn
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
- TruncateTable,
24
+ Truncate,
28
25
  )
29
- from sqlspec.statement.builder.delete import Delete
30
- from sqlspec.statement.builder.insert import Insert
31
- from sqlspec.statement.builder.merge import Merge
32
- from sqlspec.statement.builder.mixins import WhereClauseMixin
33
- from sqlspec.statement.builder.select import Select
34
- from sqlspec.statement.builder.update import Update
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
- "TruncateTable",
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. Enhanced with SQLGlot's
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 dataclasses import dataclass, field
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.statement.result import SQLResult
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: str
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
- @dataclass
43
- class QueryBuilder(ABC, Generic[RowT]):
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 automatic query optimization using SQLGlot's
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
- dialect: DialectType = field(default=None)
58
- schema: Optional[dict[str, dict[str, str]]] = field(default=None)
59
- _expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
60
- _parameters: dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False, hash=False)
61
- _parameter_counter: int = field(default=0, init=False, repr=False, compare=False, hash=False)
62
- _with_ctes: dict[str, exp.CTE] = field(default_factory=dict, init=False, repr=False, compare=False, hash=False)
63
- enable_optimization: bool = field(default=True, init=True)
64
- optimize_joins: bool = field(default=True, init=True)
65
- optimize_predicates: bool = field(default=True, init=True)
66
- simplify_expressions: bool = field(default=True, init=True)
67
-
68
- def __post_init__(self) -> None:
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
- exp.Expression: A new sqlglot expression.
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[RowT]]":
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
- # Skip boolean literals (TRUE/FALSE) and NULL
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=True)
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
- param_name_to_use = name
172
- else:
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._parameters[param_name_to_use] = value
177
- return self, param_name_to_use
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 = 1
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
- def with_cte(self: Self, alias: str, query: "Union[QueryBuilder[Any], exp.Select, str]") -> Self:
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.copy()
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.copy()
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 # This line won't be reached but satisfies type checkers
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) # pyright: ignore[reportAttributeAccessIssue]
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
- # Use SQLGlot's comprehensive optimizer
297
- return optimize(
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[SQLConfig]" = None) -> "SQL":
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.sql import SQLConfig
430
+ from sqlspec.core.statement import StatementConfig
337
431
 
338
- config = SQLConfig(dialect=safe_query.dialect)
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, config=config, **kwargs)
438
+ return SQL(safe_query.sql, statement_config=config, **kwargs)
343
439
  if parameters:
344
- return SQL(safe_query.sql, *parameters, config=config)
345
- return SQL(safe_query.sql, config=config)
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
- if hasattr(self.dialect, "__name__"):
464
+ try:
369
465
  return self.dialect.__name__.lower()
466
+ except AttributeError:
467
+ pass
370
468
  return None
371
469
 
372
470
  @property