sqlspec 0.14.0__py3-none-any.whl → 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlspec might be problematic. Click here for more details.

Files changed (158) hide show
  1. sqlspec/__init__.py +50 -25
  2. sqlspec/__main__.py +12 -0
  3. sqlspec/__metadata__.py +1 -3
  4. sqlspec/_serialization.py +1 -2
  5. sqlspec/_sql.py +256 -120
  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 +115 -248
  10. sqlspec/adapters/adbc/driver.py +462 -353
  11. sqlspec/adapters/aiosqlite/__init__.py +18 -3
  12. sqlspec/adapters/aiosqlite/_types.py +13 -0
  13. sqlspec/adapters/aiosqlite/config.py +199 -129
  14. sqlspec/adapters/aiosqlite/driver.py +230 -269
  15. sqlspec/adapters/asyncmy/__init__.py +18 -3
  16. sqlspec/adapters/asyncmy/_types.py +12 -0
  17. sqlspec/adapters/asyncmy/config.py +80 -168
  18. sqlspec/adapters/asyncmy/driver.py +260 -225
  19. sqlspec/adapters/asyncpg/__init__.py +19 -4
  20. sqlspec/adapters/asyncpg/_types.py +17 -0
  21. sqlspec/adapters/asyncpg/config.py +82 -181
  22. sqlspec/adapters/asyncpg/driver.py +285 -383
  23. sqlspec/adapters/bigquery/__init__.py +17 -3
  24. sqlspec/adapters/bigquery/_types.py +12 -0
  25. sqlspec/adapters/bigquery/config.py +191 -258
  26. sqlspec/adapters/bigquery/driver.py +474 -646
  27. sqlspec/adapters/duckdb/__init__.py +14 -3
  28. sqlspec/adapters/duckdb/_types.py +12 -0
  29. sqlspec/adapters/duckdb/config.py +415 -351
  30. sqlspec/adapters/duckdb/driver.py +343 -413
  31. sqlspec/adapters/oracledb/__init__.py +19 -5
  32. sqlspec/adapters/oracledb/_types.py +14 -0
  33. sqlspec/adapters/oracledb/config.py +123 -379
  34. sqlspec/adapters/oracledb/driver.py +507 -560
  35. sqlspec/adapters/psqlpy/__init__.py +13 -3
  36. sqlspec/adapters/psqlpy/_types.py +11 -0
  37. sqlspec/adapters/psqlpy/config.py +93 -254
  38. sqlspec/adapters/psqlpy/driver.py +505 -234
  39. sqlspec/adapters/psycopg/__init__.py +19 -5
  40. sqlspec/adapters/psycopg/_types.py +17 -0
  41. sqlspec/adapters/psycopg/config.py +143 -403
  42. sqlspec/adapters/psycopg/driver.py +706 -872
  43. sqlspec/adapters/sqlite/__init__.py +14 -3
  44. sqlspec/adapters/sqlite/_types.py +11 -0
  45. sqlspec/adapters/sqlite/config.py +202 -118
  46. sqlspec/adapters/sqlite/driver.py +264 -303
  47. sqlspec/base.py +105 -9
  48. sqlspec/{statement/builder → builder}/__init__.py +12 -14
  49. sqlspec/{statement/builder → builder}/_base.py +120 -55
  50. sqlspec/{statement/builder → builder}/_column.py +17 -6
  51. sqlspec/{statement/builder → builder}/_ddl.py +46 -79
  52. sqlspec/{statement/builder → builder}/_ddl_utils.py +5 -10
  53. sqlspec/{statement/builder → builder}/_delete.py +6 -25
  54. sqlspec/{statement/builder → builder}/_insert.py +6 -64
  55. sqlspec/builder/_merge.py +56 -0
  56. sqlspec/{statement/builder → builder}/_parsing_utils.py +3 -10
  57. sqlspec/{statement/builder → builder}/_select.py +11 -56
  58. sqlspec/{statement/builder → builder}/_update.py +12 -18
  59. sqlspec/{statement/builder → builder}/mixins/__init__.py +10 -14
  60. sqlspec/{statement/builder → builder}/mixins/_cte_and_set_ops.py +48 -59
  61. sqlspec/{statement/builder → builder}/mixins/_insert_operations.py +22 -16
  62. sqlspec/{statement/builder → builder}/mixins/_join_operations.py +1 -3
  63. sqlspec/{statement/builder → builder}/mixins/_merge_operations.py +3 -5
  64. sqlspec/{statement/builder → builder}/mixins/_order_limit_operations.py +3 -3
  65. sqlspec/{statement/builder → builder}/mixins/_pivot_operations.py +4 -8
  66. sqlspec/{statement/builder → builder}/mixins/_select_operations.py +21 -36
  67. sqlspec/{statement/builder → builder}/mixins/_update_operations.py +3 -14
  68. sqlspec/{statement/builder → builder}/mixins/_where_clause.py +52 -79
  69. sqlspec/cli.py +4 -5
  70. sqlspec/config.py +180 -133
  71. sqlspec/core/__init__.py +63 -0
  72. sqlspec/core/cache.py +873 -0
  73. sqlspec/core/compiler.py +396 -0
  74. sqlspec/core/filters.py +828 -0
  75. sqlspec/core/hashing.py +310 -0
  76. sqlspec/core/parameters.py +1209 -0
  77. sqlspec/core/result.py +664 -0
  78. sqlspec/{statement → core}/splitter.py +321 -191
  79. sqlspec/core/statement.py +651 -0
  80. sqlspec/driver/__init__.py +7 -10
  81. sqlspec/driver/_async.py +387 -176
  82. sqlspec/driver/_common.py +527 -289
  83. sqlspec/driver/_sync.py +390 -172
  84. sqlspec/driver/mixins/__init__.py +2 -19
  85. sqlspec/driver/mixins/_result_tools.py +168 -0
  86. sqlspec/driver/mixins/_sql_translator.py +6 -3
  87. sqlspec/exceptions.py +5 -252
  88. sqlspec/extensions/aiosql/adapter.py +93 -96
  89. sqlspec/extensions/litestar/config.py +0 -1
  90. sqlspec/extensions/litestar/handlers.py +15 -26
  91. sqlspec/extensions/litestar/plugin.py +16 -14
  92. sqlspec/extensions/litestar/providers.py +17 -52
  93. sqlspec/loader.py +424 -105
  94. sqlspec/migrations/__init__.py +12 -0
  95. sqlspec/migrations/base.py +92 -68
  96. sqlspec/migrations/commands.py +24 -106
  97. sqlspec/migrations/loaders.py +402 -0
  98. sqlspec/migrations/runner.py +49 -51
  99. sqlspec/migrations/tracker.py +31 -44
  100. sqlspec/migrations/utils.py +64 -24
  101. sqlspec/protocols.py +7 -183
  102. sqlspec/storage/__init__.py +1 -1
  103. sqlspec/storage/backends/base.py +37 -40
  104. sqlspec/storage/backends/fsspec.py +136 -112
  105. sqlspec/storage/backends/obstore.py +138 -160
  106. sqlspec/storage/capabilities.py +5 -4
  107. sqlspec/storage/registry.py +57 -106
  108. sqlspec/typing.py +136 -115
  109. sqlspec/utils/__init__.py +2 -3
  110. sqlspec/utils/correlation.py +0 -3
  111. sqlspec/utils/deprecation.py +6 -6
  112. sqlspec/utils/fixtures.py +6 -6
  113. sqlspec/utils/logging.py +0 -2
  114. sqlspec/utils/module_loader.py +7 -12
  115. sqlspec/utils/singleton.py +0 -1
  116. sqlspec/utils/sync_tools.py +16 -37
  117. sqlspec/utils/text.py +12 -51
  118. sqlspec/utils/type_guards.py +443 -232
  119. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/METADATA +7 -2
  120. sqlspec-0.15.0.dist-info/RECORD +134 -0
  121. sqlspec-0.15.0.dist-info/entry_points.txt +2 -0
  122. sqlspec/driver/connection.py +0 -207
  123. sqlspec/driver/mixins/_cache.py +0 -114
  124. sqlspec/driver/mixins/_csv_writer.py +0 -91
  125. sqlspec/driver/mixins/_pipeline.py +0 -508
  126. sqlspec/driver/mixins/_query_tools.py +0 -796
  127. sqlspec/driver/mixins/_result_utils.py +0 -138
  128. sqlspec/driver/mixins/_storage.py +0 -912
  129. sqlspec/driver/mixins/_type_coercion.py +0 -128
  130. sqlspec/driver/parameters.py +0 -138
  131. sqlspec/statement/__init__.py +0 -21
  132. sqlspec/statement/builder/_merge.py +0 -95
  133. sqlspec/statement/cache.py +0 -50
  134. sqlspec/statement/filters.py +0 -625
  135. sqlspec/statement/parameters.py +0 -996
  136. sqlspec/statement/pipelines/__init__.py +0 -210
  137. sqlspec/statement/pipelines/analyzers/__init__.py +0 -9
  138. sqlspec/statement/pipelines/analyzers/_analyzer.py +0 -646
  139. sqlspec/statement/pipelines/context.py +0 -115
  140. sqlspec/statement/pipelines/transformers/__init__.py +0 -7
  141. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +0 -88
  142. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +0 -1247
  143. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +0 -76
  144. sqlspec/statement/pipelines/validators/__init__.py +0 -23
  145. sqlspec/statement/pipelines/validators/_dml_safety.py +0 -290
  146. sqlspec/statement/pipelines/validators/_parameter_style.py +0 -370
  147. sqlspec/statement/pipelines/validators/_performance.py +0 -714
  148. sqlspec/statement/pipelines/validators/_security.py +0 -967
  149. sqlspec/statement/result.py +0 -435
  150. sqlspec/statement/sql.py +0 -1774
  151. sqlspec/utils/cached_property.py +0 -25
  152. sqlspec/utils/statement_hashing.py +0 -203
  153. sqlspec-0.14.0.dist-info/RECORD +0 -143
  154. sqlspec-0.14.0.dist-info/entry_points.txt +0 -2
  155. /sqlspec/{statement/builder → builder}/mixins/_delete_operations.py +0 -0
  156. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/WHEEL +0 -0
  157. {sqlspec-0.14.0.dist-info → sqlspec-0.15.0.dist-info}/licenses/LICENSE +0 -0
  158. {sqlspec-0.14.0.dist-info → sqlspec-0.15.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
- """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._select import Select
33
- from sqlspec.statement.builder._update import Update
34
- from sqlspec.statement.builder.mixins import WhereClauseMixin
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,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. 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
8
  from dataclasses import dataclass, field
10
- from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union, cast
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.statement.result import SQLResult
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, Generic[RowT]):
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 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
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
- exp.Expression: A new sqlglot expression.
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[RowT]]":
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
- # Skip boolean literals (TRUE/FALSE) and NULL
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 with_cte(self: Self, alias: str, query: "Union[QueryBuilder[Any], exp.Select, str]") -> Self:
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 # This line won't be reached but satisfies type checkers
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) # pyright: ignore[reportAttributeAccessIssue]
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
- # 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
- },
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[SQLConfig]" = None) -> "SQL":
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.sql import SQLConfig
399
+ from sqlspec.core.statement import StatementConfig
337
400
 
338
- config = SQLConfig(dialect=safe_query.dialect)
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, config=config, **kwargs)
407
+ return SQL(safe_query.sql, statement_config=config, **kwargs)
343
408
  if parameters:
344
- return SQL(safe_query.sql, *parameters, config=config)
345
- return SQL(safe_query.sql, config=config)
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.Literal.number(decimals)))
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.Literal.number(start)]
192
+ args = [exp.convert(start)]
190
193
  if length is not None:
191
- args.append(exp.Literal.number(length))
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))