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
@@ -6,13 +6,13 @@ with automatic parameter binding and validation.
6
6
 
7
7
  import re
8
8
  from dataclasses import dataclass, field
9
- from typing import Any, Optional, Union, cast
9
+ from typing import Any, Optional, Union
10
10
 
11
11
  from sqlglot import exp
12
12
  from typing_extensions import Self
13
13
 
14
- from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
15
- from sqlspec.statement.builder.mixins import (
14
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
15
+ from sqlspec.builder.mixins import (
16
16
  CommonTableExpressionMixin,
17
17
  HavingClauseMixin,
18
18
  JoinClauseMixin,
@@ -24,19 +24,17 @@ from sqlspec.statement.builder.mixins import (
24
24
  UnpivotClauseMixin,
25
25
  WhereClauseMixin,
26
26
  )
27
- from sqlspec.statement.result import SQLResult
28
- from sqlspec.typing import RowT
27
+ from sqlspec.core.result import SQLResult
29
28
 
30
29
  __all__ = ("Select",)
31
30
 
32
31
 
33
- # This pattern is formatted with the table name and compiled for each hint.
34
32
  TABLE_HINT_PATTERN = r"\b{}\b(\s+AS\s+\w+)?"
35
33
 
36
34
 
37
35
  @dataclass
38
36
  class Select(
39
- QueryBuilder[RowT],
37
+ QueryBuilder,
40
38
  WhereClauseMixin,
41
39
  OrderByClauseMixin,
42
40
  LimitOffsetClauseMixin,
@@ -51,29 +49,17 @@ class Select(
51
49
  """Type-safe builder for SELECT queries with schema/model integration.
52
50
 
53
51
  This builder provides a fluent, safe interface for constructing SQL SELECT statements.
54
- It supports type-safe result mapping via the `as_schema()` method, allowing users to
55
- associate a schema/model (such as a Pydantic model, dataclass, or msgspec.Struct) with
56
- the query for static type checking and IDE support.
57
52
 
58
53
  Example:
59
54
  >>> class User(BaseModel):
60
55
  ... id: int
61
56
  ... name: str
62
- >>> builder = (
63
- ... SelectBuilder()
64
- ... .select("id", "name")
65
- ... .from_("users")
66
- ... .as_schema(User)
67
- ... )
68
- >>> result: list[User] = driver.execute(builder)
69
-
70
- Attributes:
71
- _schema: The schema/model class for row typing, if set via as_schema().
57
+ >>> builder = Select("id", "name").from_("users")
58
+ >>> result = driver.execute(builder)
72
59
  """
73
60
 
74
61
  _with_parts: "dict[str, Union[exp.CTE, Select]]" = field(default_factory=dict, init=False)
75
62
  _expression: Optional[exp.Expression] = field(default=None, init=False, repr=False, compare=False, hash=False)
76
- _schema: Optional[type[RowT]] = None
77
63
  _hints: "list[dict[str, object]]" = field(default_factory=list, init=False, repr=False)
78
64
 
79
65
  def __init__(self, *columns: str, **kwargs: Any) -> None:
@@ -85,59 +71,33 @@ class Select(
85
71
 
86
72
  Examples:
87
73
  Select("id", "name") # Shorthand for Select().select("id", "name")
88
- Select() # Same as SelectBuilder() - start empty
74
+ Select() # Same as Select() - start empty
89
75
  """
90
76
  super().__init__(**kwargs)
91
77
 
92
- # Manually initialize dataclass fields here because a custom __init__ is defined.
93
- # This is necessary to support the `Select("col1", "col2")` shorthand initialization.
94
78
  self._with_parts = {}
95
79
  self._expression = None
96
- self._schema = None
97
80
  self._hints = []
98
81
 
99
82
  self._create_base_expression()
100
83
 
101
- # Add columns if provided - just a shorthand for .select()
102
84
  if columns:
103
85
  self.select(*columns)
104
86
 
105
87
  @property
106
- def _expected_result_type(self) -> "type[SQLResult[RowT]]":
88
+ def _expected_result_type(self) -> "type[SQLResult]":
107
89
  """Get the expected result type for SELECT operations.
108
90
 
109
91
  Returns:
110
92
  type: The SelectResult type.
111
93
  """
112
- return SQLResult[RowT]
94
+ return SQLResult
113
95
 
114
96
  def _create_base_expression(self) -> "exp.Select":
115
97
  if self._expression is None or not isinstance(self._expression, exp.Select):
116
98
  self._expression = exp.Select()
117
- # At this point, self._expression is exp.Select
118
99
  return self._expression
119
100
 
120
- def as_schema(self, schema: "type[RowT]") -> "Select[RowT]":
121
- """Return a new Select instance parameterized with the given schema/model type.
122
-
123
- This enables type-safe result mapping: the returned builder will carry the schema type
124
- for static analysis and IDE autocompletion. The schema should be a class such as a Pydantic
125
- model, dataclass, or msgspec.Struct that describes the expected row shape.
126
-
127
- Args:
128
- schema: The schema/model class to use for row typing (e.g., a Pydantic model, dataclass, or msgspec.Struct).
129
-
130
- Returns:
131
- Select[RowT]: A new Select instance with RowT set to the provided schema/model type.
132
- """
133
- new_builder = Select()
134
- new_builder._expression = self._expression.copy() if self._expression is not None else None
135
- new_builder._parameters = self._parameters.copy()
136
- new_builder._parameter_counter = self._parameter_counter
137
- new_builder.dialect = self.dialect
138
- new_builder._schema = schema # type: ignore[assignment]
139
- return cast("Select[RowT]", new_builder)
140
-
141
101
  def with_hint(
142
102
  self,
143
103
  hint: "str",
@@ -176,13 +136,11 @@ class Select(
176
136
  if isinstance(modified_expr, exp.Select):
177
137
  statement_hints = [h["hint"] for h in self._hints if h.get("location") == "statement"]
178
138
  if statement_hints:
179
- # Parse each hint and create proper hint expressions
180
139
  hint_expressions = []
181
140
 
182
141
  def parse_hint(hint: Any) -> exp.Expression:
183
- """Parse a single hint with error handling."""
142
+ """Parse a single hint."""
184
143
  try:
185
- # Try to parse hint as an expression (e.g., "INDEX(users idx_name)")
186
144
  hint_str = str(hint) # Ensure hint is a string
187
145
  hint_expr: Optional[exp.Expression] = exp.maybe_parse(hint_str, dialect=self.dialect_name)
188
146
  if hint_expr:
@@ -197,8 +155,6 @@ class Select(
197
155
  hint_node = exp.Hint(expressions=hint_expressions)
198
156
  modified_expr.set("hint", hint_node)
199
157
 
200
- # For table-level hints, we'll fall back to comment injection in SQL
201
- # since SQLGlot doesn't have a standard way to attach hints to individual tables
202
158
  modified_sql = modified_expr.sql(dialect=self.dialect_name, pretty=True)
203
159
 
204
160
  table_hints = [h for h in self._hints if h.get("location") == "table" and h.get("table")]
@@ -206,7 +162,6 @@ class Select(
206
162
  for th in table_hints:
207
163
  table = str(th["table"])
208
164
  hint = th["hint"]
209
- # More precise regex that captures the table and optional alias
210
165
  pattern = TABLE_HINT_PATTERN.format(re.escape(table))
211
166
  compiled_pattern = re.compile(pattern, re.IGNORECASE)
212
167
 
@@ -10,27 +10,26 @@ from typing import TYPE_CHECKING, Any, Optional, Union
10
10
  from sqlglot import exp
11
11
  from typing_extensions import Self
12
12
 
13
- from sqlspec.exceptions import SQLBuilderError
14
- from sqlspec.statement.builder._base import QueryBuilder, SafeQuery
15
- from sqlspec.statement.builder.mixins import (
13
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
14
+ from sqlspec.builder.mixins import (
16
15
  ReturningClauseMixin,
17
16
  UpdateFromClauseMixin,
18
17
  UpdateSetClauseMixin,
19
18
  UpdateTableClauseMixin,
20
19
  WhereClauseMixin,
21
20
  )
22
- from sqlspec.statement.result import SQLResult
23
- from sqlspec.typing import RowT
21
+ from sqlspec.core.result import SQLResult
22
+ from sqlspec.exceptions import SQLBuilderError
24
23
 
25
24
  if TYPE_CHECKING:
26
- from sqlspec.statement.builder._select import Select
25
+ from sqlspec.builder._select import Select
27
26
 
28
27
  __all__ = ("Update",)
29
28
 
30
29
 
31
30
  @dataclass(unsafe_hash=True)
32
31
  class Update(
33
- QueryBuilder[RowT],
32
+ QueryBuilder,
34
33
  WhereClauseMixin,
35
34
  ReturningClauseMixin,
36
35
  UpdateSetClauseMixin,
@@ -44,7 +43,6 @@ class Update(
44
43
 
45
44
  Example:
46
45
  ```python
47
- # Basic UPDATE
48
46
  update_query = (
49
47
  Update()
50
48
  .table("users")
@@ -53,12 +51,10 @@ class Update(
53
51
  .where("id = 1")
54
52
  )
55
53
 
56
- # Even more concise with constructor
57
54
  update_query = (
58
55
  Update("users").set(name="John Doe").where("id = 1")
59
56
  )
60
57
 
61
- # UPDATE with parameterized conditions
62
58
  update_query = (
63
59
  Update()
64
60
  .table("users")
@@ -66,7 +62,6 @@ class Update(
66
62
  .where_eq("id", 123)
67
63
  )
68
64
 
69
- # UPDATE with FROM clause (PostgreSQL style)
70
65
  update_query = (
71
66
  Update()
72
67
  .table("users", "u")
@@ -90,9 +85,9 @@ class Update(
90
85
  self.table(table)
91
86
 
92
87
  @property
93
- def _expected_result_type(self) -> "type[SQLResult[RowT]]":
88
+ def _expected_result_type(self) -> "type[SQLResult]":
94
89
  """Return the expected result type for this builder."""
95
- return SQLResult[RowT]
90
+ return SQLResult
96
91
 
97
92
  def _create_base_expression(self) -> exp.Update:
98
93
  """Create a base UPDATE expression.
@@ -104,7 +99,7 @@ class Update(
104
99
 
105
100
  def join(
106
101
  self,
107
- table: "Union[str, exp.Expression, Select[RowT]]",
102
+ table: "Union[str, exp.Expression, Select]",
108
103
  on: "Union[str, exp.Expression]",
109
104
  alias: "Optional[str]" = None,
110
105
  join_type: str = "INNER",
@@ -135,10 +130,9 @@ class Update(
135
130
  subquery_exp = exp.paren(exp.maybe_parse(subquery.sql, dialect=self.dialect))
136
131
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
137
132
 
138
- # Merge parameters
139
- subquery_params = table._parameters
140
- if subquery_params:
141
- for p_name, p_value in subquery_params.items():
133
+ subquery_parameters = table._parameters
134
+ if subquery_parameters:
135
+ for p_name, p_value in subquery_parameters.items():
142
136
  self.add_parameter(p_value, name=p_name)
143
137
  else:
144
138
  table_expr = table
@@ -1,14 +1,10 @@
1
1
  """SQL statement builder mixins."""
2
2
 
3
- from sqlspec.statement.builder.mixins._cte_and_set_ops import CommonTableExpressionMixin, SetOperationMixin
4
- from sqlspec.statement.builder.mixins._delete_operations import DeleteFromClauseMixin
5
- from sqlspec.statement.builder.mixins._insert_operations import (
6
- InsertFromSelectMixin,
7
- InsertIntoClauseMixin,
8
- InsertValuesMixin,
9
- )
10
- from sqlspec.statement.builder.mixins._join_operations import JoinClauseMixin
11
- from sqlspec.statement.builder.mixins._merge_operations import (
3
+ from sqlspec.builder.mixins._cte_and_set_ops import CommonTableExpressionMixin, SetOperationMixin
4
+ from sqlspec.builder.mixins._delete_operations import DeleteFromClauseMixin
5
+ from sqlspec.builder.mixins._insert_operations import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin
6
+ from sqlspec.builder.mixins._join_operations import JoinClauseMixin
7
+ from sqlspec.builder.mixins._merge_operations import (
12
8
  MergeIntoClauseMixin,
13
9
  MergeMatchedClauseMixin,
14
10
  MergeNotMatchedBySourceClauseMixin,
@@ -16,19 +12,19 @@ from sqlspec.statement.builder.mixins._merge_operations import (
16
12
  MergeOnClauseMixin,
17
13
  MergeUsingClauseMixin,
18
14
  )
19
- from sqlspec.statement.builder.mixins._order_limit_operations import (
15
+ from sqlspec.builder.mixins._order_limit_operations import (
20
16
  LimitOffsetClauseMixin,
21
17
  OrderByClauseMixin,
22
18
  ReturningClauseMixin,
23
19
  )
24
- from sqlspec.statement.builder.mixins._pivot_operations import PivotClauseMixin, UnpivotClauseMixin
25
- from sqlspec.statement.builder.mixins._select_operations import CaseBuilder, SelectClauseMixin
26
- from sqlspec.statement.builder.mixins._update_operations import (
20
+ from sqlspec.builder.mixins._pivot_operations import PivotClauseMixin, UnpivotClauseMixin
21
+ from sqlspec.builder.mixins._select_operations import CaseBuilder, SelectClauseMixin
22
+ from sqlspec.builder.mixins._update_operations import (
27
23
  UpdateFromClauseMixin,
28
24
  UpdateSetClauseMixin,
29
25
  UpdateTableClauseMixin,
30
26
  )
31
- from sqlspec.statement.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
27
+ from sqlspec.builder.mixins._where_clause import HavingClauseMixin, WhereClauseMixin
32
28
 
33
29
  __all__ = (
34
30
  "CaseBuilder",
@@ -36,57 +36,49 @@ class CommonTableExpressionMixin:
36
36
  msg = "Cannot add WITH clause: expression not initialized."
37
37
  raise SQLBuilderError(msg)
38
38
 
39
- if not hasattr(self._expression, "with_") and not isinstance(
40
- self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)
41
- ):
39
+ if not isinstance(self._expression, (exp.Select, exp.Insert, exp.Update, exp.Delete)):
42
40
  msg = f"Cannot add WITH clause to {type(self._expression).__name__} expression."
43
41
  raise SQLBuilderError(msg)
44
42
 
45
43
  cte_expr: Optional[exp.Expression] = None
46
- if hasattr(query, "to_statement"):
47
- # Query is a builder instance
48
- built_query = query.to_statement() # pyright: ignore
49
- cte_sql = built_query.to_sql()
50
- cte_expr = exp.maybe_parse(cte_sql, dialect=getattr(self, "dialect", None))
51
-
52
- # Merge parameters
53
- if hasattr(self, "add_parameter"):
54
- parameters = getattr(built_query, "parameters", None) or {}
55
- for param_name, param_value in parameters.items():
56
- self.add_parameter(param_value, name=param_name) # pyright: ignore
57
- elif isinstance(query, str):
58
- cte_expr = exp.maybe_parse(query, dialect=getattr(self, "dialect", None))
44
+ if isinstance(query, str):
45
+ cte_expr = exp.maybe_parse(query, dialect=self.dialect) # type: ignore[attr-defined]
59
46
  elif isinstance(query, exp.Expression):
60
47
  cte_expr = query
48
+ else:
49
+ built_query = query.to_statement() # pyright: ignore
50
+ cte_sql = built_query.sql
51
+ cte_expr = exp.maybe_parse(cte_sql, dialect=self.dialect) # type: ignore[attr-defined]
52
+
53
+ parameters = built_query.parameters
54
+ if parameters:
55
+ if isinstance(parameters, dict):
56
+ for param_name, param_value in parameters.items():
57
+ self.add_parameter(param_value, name=param_name) # type: ignore[attr-defined]
58
+ elif isinstance(parameters, (list, tuple)):
59
+ for param_value in parameters:
60
+ self.add_parameter(param_value) # type: ignore[attr-defined]
61
61
 
62
62
  if not cte_expr:
63
63
  msg = f"Could not parse CTE query: {query}"
64
64
  raise SQLBuilderError(msg)
65
65
 
66
66
  if columns:
67
- # CTE with explicit column list: name(col1, col2, ...)
68
67
  cte_alias_expr = exp.alias_(cte_expr, name, table=[exp.to_identifier(col) for col in columns])
69
68
  else:
70
- # Simple CTE alias: name
71
69
  cte_alias_expr = exp.alias_(cte_expr, name)
72
70
 
73
- # Different handling for different expression types
74
- if hasattr(self._expression, "with_"):
75
- existing_with = self._expression.args.get("with") # pyright: ignore
76
- if existing_with:
77
- existing_with.expressions.append(cte_alias_expr)
78
- if recursive:
79
- existing_with.set("recursive", recursive)
80
- else:
81
- self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # pyright: ignore
82
- if recursive:
83
- with_clause = self._expression.find(exp.With)
84
- if with_clause:
85
- with_clause.set("recursive", recursive)
71
+ existing_with = self._expression.args.get("with") # pyright: ignore
72
+ if existing_with:
73
+ existing_with.expressions.append(cte_alias_expr)
74
+ if recursive:
75
+ existing_with.set("recursive", recursive)
86
76
  else:
87
- # Store CTEs for later application during build
88
- if not hasattr(self, "_with_ctes"):
89
- setattr(self, "_with_ctes", {})
77
+ self._expression = self._expression.with_(cte_alias_expr, as_=name, copy=False) # type: ignore[union-attr]
78
+ if recursive:
79
+ with_clause = self._expression.find(exp.With)
80
+ if with_clause:
81
+ with_clause.set("recursive", recursive)
90
82
  self._with_ctes[name] = exp.CTE(this=cte_expr, alias=exp.to_table(name)) # type: ignore[attr-defined]
91
83
 
92
84
  return self
@@ -96,7 +88,7 @@ class SetOperationMixin:
96
88
  """Mixin providing set operations (UNION, INTERSECT, EXCEPT) for SELECT builders."""
97
89
 
98
90
  _expression: Any = None
99
- _parameters: dict[str, Any] = {}
91
+ _parameters: dict[str, Any]
100
92
  dialect: Any = None
101
93
 
102
94
  def union(self, other: Any, all_: bool = False) -> Self:
@@ -114,25 +106,24 @@ class SetOperationMixin:
114
106
  """
115
107
  left_query = self.build() # type: ignore[attr-defined]
116
108
  right_query = other.build()
117
- left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
118
- right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
109
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
110
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
119
111
  if not left_expr or not right_expr:
120
112
  msg = "Could not parse queries for UNION operation"
121
113
  raise SQLBuilderError(msg)
122
114
  union_expr = exp.union(left_expr, right_expr, distinct=not all_)
123
115
  new_builder = type(self)()
124
- new_builder.dialect = getattr(self, "dialect", None)
116
+ new_builder.dialect = self.dialect
125
117
  new_builder._expression = union_expr
126
- merged_params = dict(left_query.parameters)
118
+ merged_parameters = dict(left_query.parameters)
127
119
  for param_name, param_value in right_query.parameters.items():
128
- if param_name in merged_params:
120
+ if param_name in merged_parameters:
129
121
  counter = 1
130
122
  new_param_name = f"{param_name}_right_{counter}"
131
- while new_param_name in merged_params:
123
+ while new_param_name in merged_parameters:
132
124
  counter += 1
133
125
  new_param_name = f"{param_name}_right_{counter}"
134
126
 
135
- # Use AST transformation instead of string manipulation
136
127
  def rename_parameter(node: exp.Expression) -> exp.Expression:
137
128
  if isinstance(node, exp.Placeholder) and node.name == param_name: # noqa: B023
138
129
  return exp.Placeholder(this=new_param_name) # noqa: B023
@@ -141,10 +132,10 @@ class SetOperationMixin:
141
132
  right_expr = right_expr.transform(rename_parameter)
142
133
  union_expr = exp.union(left_expr, right_expr, distinct=not all_)
143
134
  new_builder._expression = union_expr
144
- merged_params[new_param_name] = param_value
135
+ merged_parameters[new_param_name] = param_value
145
136
  else:
146
- merged_params[param_name] = param_value
147
- new_builder._parameters = merged_params
137
+ merged_parameters[param_name] = param_value
138
+ new_builder._parameters = merged_parameters
148
139
  return new_builder
149
140
 
150
141
  def intersect(self, other: Any) -> Self:
@@ -161,19 +152,18 @@ class SetOperationMixin:
161
152
  """
162
153
  left_query = self.build() # type: ignore[attr-defined]
163
154
  right_query = other.build()
164
- left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
165
- right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
155
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
156
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
166
157
  if not left_expr or not right_expr:
167
158
  msg = "Could not parse queries for INTERSECT operation"
168
159
  raise SQLBuilderError(msg)
169
160
  intersect_expr = exp.intersect(left_expr, right_expr, distinct=True)
170
161
  new_builder = type(self)()
171
- new_builder.dialect = getattr(self, "dialect", None)
162
+ new_builder.dialect = self.dialect
172
163
  new_builder._expression = intersect_expr
173
- # Merge parameters
174
- merged_params = dict(left_query.parameters)
175
- merged_params.update(right_query.parameters)
176
- new_builder._parameters = merged_params
164
+ merged_parameters = dict(left_query.parameters)
165
+ merged_parameters.update(right_query.parameters)
166
+ new_builder._parameters = merged_parameters
177
167
  return new_builder
178
168
 
179
169
  def except_(self, other: Any) -> Self:
@@ -190,17 +180,16 @@ class SetOperationMixin:
190
180
  """
191
181
  left_query = self.build() # type: ignore[attr-defined]
192
182
  right_query = other.build()
193
- left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=getattr(self, "dialect", None))
194
- right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=getattr(self, "dialect", None))
183
+ left_expr: Optional[exp.Expression] = exp.maybe_parse(left_query.sql, dialect=self.dialect)
184
+ right_expr: Optional[exp.Expression] = exp.maybe_parse(right_query.sql, dialect=self.dialect)
195
185
  if not left_expr or not right_expr:
196
186
  msg = "Could not parse queries for EXCEPT operation"
197
187
  raise SQLBuilderError(msg)
198
188
  except_expr = exp.except_(left_expr, right_expr)
199
189
  new_builder = type(self)()
200
- new_builder.dialect = getattr(self, "dialect", None)
190
+ new_builder.dialect = self.dialect
201
191
  new_builder._expression = except_expr
202
- # Merge parameters
203
- merged_params = dict(left_query.parameters)
204
- merged_params.update(right_query.parameters)
205
- new_builder._parameters = merged_params
192
+ merged_parameters = dict(left_query.parameters)
193
+ merged_parameters.update(right_query.parameters)
194
+ new_builder._parameters = merged_parameters
206
195
  return new_builder
@@ -53,13 +53,14 @@ class InsertValuesMixin:
53
53
  raise SQLBuilderError(msg)
54
54
  column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
55
55
  self._expression.set("columns", column_exprs)
56
- # Synchronize the _columns attribute on the builder (if present)
57
- if hasattr(self, "_columns"):
58
- # If no columns, clear the list
56
+ try:
57
+ cols = self._columns # type: ignore[attr-defined]
59
58
  if not columns:
60
- self._columns.clear() # pyright: ignore
59
+ cols.clear()
61
60
  else:
62
- self._columns[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns] # pyright: ignore
61
+ cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
62
+ except AttributeError:
63
+ pass
63
64
  return self
64
65
 
65
66
  def values(self, *values: Any) -> Self:
@@ -69,11 +70,13 @@ class InsertValuesMixin:
69
70
  if not isinstance(self._expression, exp.Insert):
70
71
  msg = "Cannot add values to a non-INSERT expression."
71
72
  raise SQLBuilderError(msg)
72
- if (
73
- hasattr(self, "_columns") and getattr(self, "_columns", []) and len(values) != len(self._columns) # pyright: ignore
74
- ):
75
- msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(self._columns)})." # pyright: ignore
76
- raise SQLBuilderError(msg)
73
+ try:
74
+ _columns = self._columns # type: ignore[attr-defined]
75
+ if _columns and len(values) != len(_columns):
76
+ msg = f"Number of values ({len(values)}) does not match the number of specified columns ({len(_columns)})."
77
+ raise SQLBuilderError(msg)
78
+ except AttributeError:
79
+ pass
77
80
  row_exprs = []
78
81
  for v in values:
79
82
  if isinstance(v, exp.Expression):
@@ -114,7 +117,11 @@ class InsertFromSelectMixin:
114
117
  Raises:
115
118
  SQLBuilderError: If the table is not set or the select_builder is invalid.
116
119
  """
117
- if not getattr(self, "_table", None):
120
+ try:
121
+ if not self._table: # type: ignore[attr-defined]
122
+ msg = "The target table must be set using .into() before adding values."
123
+ raise SQLBuilderError(msg)
124
+ except AttributeError:
118
125
  msg = "The target table must be set using .into() before adding values."
119
126
  raise SQLBuilderError(msg)
120
127
  if self._expression is None:
@@ -122,12 +129,11 @@ class InsertFromSelectMixin:
122
129
  if not isinstance(self._expression, exp.Insert):
123
130
  msg = "Cannot set INSERT source on a non-INSERT expression."
124
131
  raise SQLBuilderError(msg)
125
- # Merge parameters from the SELECT builder
126
- subquery_params = getattr(select_builder, "_parameters", None)
127
- if subquery_params:
128
- for p_name, p_value in subquery_params.items():
132
+ subquery_parameters = select_builder._parameters # pyright: ignore[attr-defined]
133
+ if subquery_parameters:
134
+ for p_name, p_value in subquery_parameters.items():
129
135
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
130
- select_expr = getattr(select_builder, "_expression", None)
136
+ select_expr = select_builder._expression # pyright: ignore[attr-defined]
131
137
  if select_expr and isinstance(select_expr, exp.Select):
132
138
  self._expression.set("expression", select_expr.copy())
133
139
  else:
@@ -3,8 +3,8 @@ from typing import TYPE_CHECKING, Any, Optional, Union, cast
3
3
  from sqlglot import exp
4
4
  from typing_extensions import Self
5
5
 
6
+ from sqlspec.builder._parsing_utils import parse_table_expression
6
7
  from sqlspec.exceptions import SQLBuilderError
7
- from sqlspec.statement.builder._parsing_utils import parse_table_expression
8
8
  from sqlspec.utils.type_guards import has_query_builder_parameters
9
9
 
10
10
  if TYPE_CHECKING:
@@ -33,7 +33,6 @@ class JoinClauseMixin:
33
33
  if isinstance(table, str):
34
34
  table_expr = parse_table_expression(table, alias)
35
35
  elif has_query_builder_parameters(table):
36
- # Work directly with AST when possible to avoid string parsing
37
36
  if hasattr(table, "_expression") and getattr(table, "_expression", None) is not None:
38
37
  table_expr_value = getattr(table, "_expression", None)
39
38
  if table_expr_value is not None:
@@ -46,7 +45,6 @@ class JoinClauseMixin:
46
45
  sql_str = subquery.sql if hasattr(subquery, "sql") and not callable(subquery.sql) else str(subquery)
47
46
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=getattr(builder, "dialect", None)))
48
47
  table_expr = exp.alias_(subquery_exp, alias) if alias else subquery_exp
49
- # Parameter merging logic can be added here if needed
50
48
  else:
51
49
  table_expr = table
52
50
  on_expr: Optional[exp.Expression] = None
@@ -70,10 +70,9 @@ class MergeUsingClauseMixin:
70
70
  if isinstance(source, str):
71
71
  source_expr = exp.to_table(source, alias=alias)
72
72
  elif has_query_builder_parameters(source) and hasattr(source, "_expression"):
73
- # Merge parameters from the SELECT builder or other builder
74
- subquery_builder_params = source.parameters
75
- if subquery_builder_params:
76
- for p_name, p_value in subquery_builder_params.items():
73
+ subquery_builder_parameters = source.parameters
74
+ if subquery_builder_parameters:
75
+ for p_name, p_value in subquery_builder_parameters.items():
77
76
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
78
77
 
79
78
  subquery_exp = exp.paren(getattr(source, "_expression", exp.select()))
@@ -281,7 +280,6 @@ class MergeNotMatchedClauseMixin:
281
280
  msg = "Specifying columns without values for INSERT action is complex and not fully supported yet. Consider providing full expressions."
282
281
  raise SQLBuilderError(msg)
283
282
  elif not columns and not values:
284
- # INSERT DEFAULT VALUES case
285
283
  pass
286
284
  else:
287
285
  msg = "Cannot specify values without columns for INSERT action."
@@ -5,8 +5,8 @@ from typing import TYPE_CHECKING, Optional, Union, cast
5
5
  from sqlglot import exp
6
6
  from typing_extensions import Self
7
7
 
8
+ from sqlspec.builder._parsing_utils import parse_order_expression
8
9
  from sqlspec.exceptions import SQLBuilderError
9
- from sqlspec.statement.builder._parsing_utils import parse_order_expression
10
10
 
11
11
  if TYPE_CHECKING:
12
12
  from sqlspec.protocols import SQLBuilderProtocol
@@ -71,7 +71,7 @@ class LimitOffsetClauseMixin:
71
71
  if not isinstance(builder._expression, exp.Select):
72
72
  msg = "LIMIT is only supported for SELECT statements."
73
73
  raise SQLBuilderError(msg)
74
- builder._expression = builder._expression.limit(exp.Literal.number(value), copy=False)
74
+ builder._expression = builder._expression.limit(exp.convert(value), copy=False)
75
75
  return cast("Self", builder)
76
76
 
77
77
  def offset(self, value: int) -> Self:
@@ -90,7 +90,7 @@ class LimitOffsetClauseMixin:
90
90
  if not isinstance(builder._expression, exp.Select):
91
91
  msg = "OFFSET is only supported for SELECT statements."
92
92
  raise SQLBuilderError(msg)
93
- builder._expression = builder._expression.offset(exp.Literal.number(value), copy=False)
93
+ builder._expression = builder._expression.offset(exp.convert(value), copy=False)
94
94
  return cast("Self", builder)
95
95
 
96
96