sqlspec 0.16.0__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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 (148) hide show
  1. 51ff5a9eadfdefd49f98__mypyc.cpython-313-x86_64-linux-gnu.so +0 -0
  2. sqlspec/__init__.py +92 -0
  3. sqlspec/__main__.py +12 -0
  4. sqlspec/__metadata__.py +14 -0
  5. sqlspec/_serialization.py +77 -0
  6. sqlspec/_sql.py +1347 -0
  7. sqlspec/_typing.py +680 -0
  8. sqlspec/adapters/__init__.py +0 -0
  9. sqlspec/adapters/adbc/__init__.py +5 -0
  10. sqlspec/adapters/adbc/_types.py +12 -0
  11. sqlspec/adapters/adbc/config.py +361 -0
  12. sqlspec/adapters/adbc/driver.py +512 -0
  13. sqlspec/adapters/aiosqlite/__init__.py +19 -0
  14. sqlspec/adapters/aiosqlite/_types.py +13 -0
  15. sqlspec/adapters/aiosqlite/config.py +253 -0
  16. sqlspec/adapters/aiosqlite/driver.py +248 -0
  17. sqlspec/adapters/asyncmy/__init__.py +19 -0
  18. sqlspec/adapters/asyncmy/_types.py +12 -0
  19. sqlspec/adapters/asyncmy/config.py +180 -0
  20. sqlspec/adapters/asyncmy/driver.py +274 -0
  21. sqlspec/adapters/asyncpg/__init__.py +21 -0
  22. sqlspec/adapters/asyncpg/_types.py +17 -0
  23. sqlspec/adapters/asyncpg/config.py +229 -0
  24. sqlspec/adapters/asyncpg/driver.py +344 -0
  25. sqlspec/adapters/bigquery/__init__.py +18 -0
  26. sqlspec/adapters/bigquery/_types.py +12 -0
  27. sqlspec/adapters/bigquery/config.py +298 -0
  28. sqlspec/adapters/bigquery/driver.py +558 -0
  29. sqlspec/adapters/duckdb/__init__.py +22 -0
  30. sqlspec/adapters/duckdb/_types.py +12 -0
  31. sqlspec/adapters/duckdb/config.py +504 -0
  32. sqlspec/adapters/duckdb/driver.py +368 -0
  33. sqlspec/adapters/oracledb/__init__.py +32 -0
  34. sqlspec/adapters/oracledb/_types.py +14 -0
  35. sqlspec/adapters/oracledb/config.py +317 -0
  36. sqlspec/adapters/oracledb/driver.py +538 -0
  37. sqlspec/adapters/psqlpy/__init__.py +16 -0
  38. sqlspec/adapters/psqlpy/_types.py +11 -0
  39. sqlspec/adapters/psqlpy/config.py +214 -0
  40. sqlspec/adapters/psqlpy/driver.py +530 -0
  41. sqlspec/adapters/psycopg/__init__.py +32 -0
  42. sqlspec/adapters/psycopg/_types.py +17 -0
  43. sqlspec/adapters/psycopg/config.py +426 -0
  44. sqlspec/adapters/psycopg/driver.py +796 -0
  45. sqlspec/adapters/sqlite/__init__.py +15 -0
  46. sqlspec/adapters/sqlite/_types.py +11 -0
  47. sqlspec/adapters/sqlite/config.py +240 -0
  48. sqlspec/adapters/sqlite/driver.py +294 -0
  49. sqlspec/base.py +571 -0
  50. sqlspec/builder/__init__.py +62 -0
  51. sqlspec/builder/_base.py +440 -0
  52. sqlspec/builder/_column.py +324 -0
  53. sqlspec/builder/_ddl.py +1383 -0
  54. sqlspec/builder/_ddl_utils.py +104 -0
  55. sqlspec/builder/_delete.py +77 -0
  56. sqlspec/builder/_insert.py +241 -0
  57. sqlspec/builder/_merge.py +56 -0
  58. sqlspec/builder/_parsing_utils.py +140 -0
  59. sqlspec/builder/_select.py +174 -0
  60. sqlspec/builder/_update.py +186 -0
  61. sqlspec/builder/mixins/__init__.py +55 -0
  62. sqlspec/builder/mixins/_cte_and_set_ops.py +195 -0
  63. sqlspec/builder/mixins/_delete_operations.py +36 -0
  64. sqlspec/builder/mixins/_insert_operations.py +152 -0
  65. sqlspec/builder/mixins/_join_operations.py +115 -0
  66. sqlspec/builder/mixins/_merge_operations.py +416 -0
  67. sqlspec/builder/mixins/_order_limit_operations.py +123 -0
  68. sqlspec/builder/mixins/_pivot_operations.py +144 -0
  69. sqlspec/builder/mixins/_select_operations.py +599 -0
  70. sqlspec/builder/mixins/_update_operations.py +164 -0
  71. sqlspec/builder/mixins/_where_clause.py +609 -0
  72. sqlspec/cli.py +247 -0
  73. sqlspec/config.py +395 -0
  74. sqlspec/core/__init__.py +63 -0
  75. sqlspec/core/cache.cpython-313-x86_64-linux-gnu.so +0 -0
  76. sqlspec/core/cache.py +873 -0
  77. sqlspec/core/compiler.cpython-313-x86_64-linux-gnu.so +0 -0
  78. sqlspec/core/compiler.py +396 -0
  79. sqlspec/core/filters.cpython-313-x86_64-linux-gnu.so +0 -0
  80. sqlspec/core/filters.py +830 -0
  81. sqlspec/core/hashing.cpython-313-x86_64-linux-gnu.so +0 -0
  82. sqlspec/core/hashing.py +310 -0
  83. sqlspec/core/parameters.cpython-313-x86_64-linux-gnu.so +0 -0
  84. sqlspec/core/parameters.py +1209 -0
  85. sqlspec/core/result.cpython-313-x86_64-linux-gnu.so +0 -0
  86. sqlspec/core/result.py +664 -0
  87. sqlspec/core/splitter.cpython-313-x86_64-linux-gnu.so +0 -0
  88. sqlspec/core/splitter.py +819 -0
  89. sqlspec/core/statement.cpython-313-x86_64-linux-gnu.so +0 -0
  90. sqlspec/core/statement.py +666 -0
  91. sqlspec/driver/__init__.py +19 -0
  92. sqlspec/driver/_async.py +472 -0
  93. sqlspec/driver/_common.py +612 -0
  94. sqlspec/driver/_sync.py +473 -0
  95. sqlspec/driver/mixins/__init__.py +6 -0
  96. sqlspec/driver/mixins/_result_tools.py +164 -0
  97. sqlspec/driver/mixins/_sql_translator.py +36 -0
  98. sqlspec/exceptions.py +193 -0
  99. sqlspec/extensions/__init__.py +0 -0
  100. sqlspec/extensions/aiosql/__init__.py +10 -0
  101. sqlspec/extensions/aiosql/adapter.py +461 -0
  102. sqlspec/extensions/litestar/__init__.py +6 -0
  103. sqlspec/extensions/litestar/_utils.py +52 -0
  104. sqlspec/extensions/litestar/cli.py +48 -0
  105. sqlspec/extensions/litestar/config.py +92 -0
  106. sqlspec/extensions/litestar/handlers.py +260 -0
  107. sqlspec/extensions/litestar/plugin.py +145 -0
  108. sqlspec/extensions/litestar/providers.py +454 -0
  109. sqlspec/loader.cpython-313-x86_64-linux-gnu.so +0 -0
  110. sqlspec/loader.py +760 -0
  111. sqlspec/migrations/__init__.py +35 -0
  112. sqlspec/migrations/base.py +414 -0
  113. sqlspec/migrations/commands.py +443 -0
  114. sqlspec/migrations/loaders.py +402 -0
  115. sqlspec/migrations/runner.py +213 -0
  116. sqlspec/migrations/tracker.py +140 -0
  117. sqlspec/migrations/utils.py +129 -0
  118. sqlspec/protocols.py +400 -0
  119. sqlspec/py.typed +0 -0
  120. sqlspec/storage/__init__.py +23 -0
  121. sqlspec/storage/backends/__init__.py +0 -0
  122. sqlspec/storage/backends/base.py +163 -0
  123. sqlspec/storage/backends/fsspec.py +386 -0
  124. sqlspec/storage/backends/obstore.py +459 -0
  125. sqlspec/storage/capabilities.py +102 -0
  126. sqlspec/storage/registry.py +239 -0
  127. sqlspec/typing.py +299 -0
  128. sqlspec/utils/__init__.py +3 -0
  129. sqlspec/utils/correlation.py +150 -0
  130. sqlspec/utils/deprecation.py +106 -0
  131. sqlspec/utils/fixtures.cpython-313-x86_64-linux-gnu.so +0 -0
  132. sqlspec/utils/fixtures.py +58 -0
  133. sqlspec/utils/logging.py +127 -0
  134. sqlspec/utils/module_loader.py +89 -0
  135. sqlspec/utils/serializers.py +4 -0
  136. sqlspec/utils/singleton.py +32 -0
  137. sqlspec/utils/sync_tools.cpython-313-x86_64-linux-gnu.so +0 -0
  138. sqlspec/utils/sync_tools.py +237 -0
  139. sqlspec/utils/text.cpython-313-x86_64-linux-gnu.so +0 -0
  140. sqlspec/utils/text.py +96 -0
  141. sqlspec/utils/type_guards.cpython-313-x86_64-linux-gnu.so +0 -0
  142. sqlspec/utils/type_guards.py +1135 -0
  143. sqlspec-0.16.0.dist-info/METADATA +365 -0
  144. sqlspec-0.16.0.dist-info/RECORD +148 -0
  145. sqlspec-0.16.0.dist-info/WHEEL +4 -0
  146. sqlspec-0.16.0.dist-info/entry_points.txt +2 -0
  147. sqlspec-0.16.0.dist-info/licenses/LICENSE +21 -0
  148. sqlspec-0.16.0.dist-info/licenses/NOTICE +29 -0
@@ -0,0 +1,104 @@
1
+ """DDL builder utilities."""
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from sqlglot import exp
6
+
7
+ if TYPE_CHECKING:
8
+ from sqlspec.builder._ddl import ColumnDefinition, ConstraintDefinition
9
+
10
+ __all__ = ("build_column_expression", "build_constraint_expression")
11
+
12
+
13
+ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
14
+ """Build SQLGlot expression for a column definition."""
15
+ col_def = exp.ColumnDef(this=exp.to_identifier(col.name), kind=exp.DataType.build(col.dtype))
16
+
17
+ constraints: list[exp.ColumnConstraint] = []
18
+
19
+ if col.not_null:
20
+ constraints.append(exp.ColumnConstraint(kind=exp.NotNullColumnConstraint()))
21
+
22
+ if col.primary_key:
23
+ constraints.append(exp.ColumnConstraint(kind=exp.PrimaryKeyColumnConstraint()))
24
+
25
+ if col.unique:
26
+ constraints.append(exp.ColumnConstraint(kind=exp.UniqueColumnConstraint()))
27
+
28
+ if col.default is not None:
29
+ default_expr: Optional[exp.Expression] = None
30
+ if isinstance(col.default, str):
31
+ if col.default.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in col.default:
32
+ default_expr = exp.maybe_parse(col.default)
33
+ else:
34
+ default_expr = exp.convert(col.default)
35
+ else:
36
+ # Use exp.convert for all other types (int, float, bool, None, etc.)
37
+ default_expr = exp.convert(col.default)
38
+
39
+ constraints.append(exp.ColumnConstraint(kind=default_expr))
40
+
41
+ if col.check:
42
+ check_expr = exp.Check(this=exp.maybe_parse(col.check))
43
+ constraints.append(exp.ColumnConstraint(kind=check_expr))
44
+
45
+ if col.comment:
46
+ constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.convert(col.comment))))
47
+
48
+ if col.generated:
49
+ generated_expr = exp.GeneratedAsIdentityColumnConstraint(this=exp.maybe_parse(col.generated))
50
+ constraints.append(exp.ColumnConstraint(kind=generated_expr))
51
+
52
+ if col.collate:
53
+ constraints.append(exp.ColumnConstraint(kind=exp.CollateColumnConstraint(this=exp.to_identifier(col.collate))))
54
+
55
+ if constraints:
56
+ col_def.set("constraints", constraints)
57
+
58
+ return col_def
59
+
60
+
61
+ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
62
+ """Build SQLGlot expression for a table constraint."""
63
+ if constraint.constraint_type == "PRIMARY KEY":
64
+ pk_cols = [exp.to_identifier(col) for col in constraint.columns]
65
+ pk_constraint = exp.PrimaryKey(expressions=pk_cols)
66
+
67
+ if constraint.name:
68
+ return exp.Constraint(this=exp.to_identifier(constraint.name), expression=pk_constraint)
69
+ return pk_constraint
70
+
71
+ if constraint.constraint_type == "FOREIGN KEY":
72
+ fk_cols = [exp.to_identifier(col) for col in constraint.columns]
73
+ ref_cols = [exp.to_identifier(col) for col in constraint.references_columns]
74
+
75
+ fk_constraint = exp.ForeignKey(
76
+ expressions=fk_cols,
77
+ reference=exp.Reference(
78
+ this=exp.to_table(constraint.references_table) if constraint.references_table else None,
79
+ expressions=ref_cols,
80
+ on_delete=constraint.on_delete,
81
+ on_update=constraint.on_update,
82
+ ),
83
+ )
84
+
85
+ if constraint.name:
86
+ return exp.Constraint(this=exp.to_identifier(constraint.name), expression=fk_constraint)
87
+ return fk_constraint
88
+
89
+ if constraint.constraint_type == "UNIQUE":
90
+ unique_cols = [exp.to_identifier(col) for col in constraint.columns]
91
+ unique_constraint = exp.UniqueKeyProperty(expressions=unique_cols)
92
+
93
+ if constraint.name:
94
+ return exp.Constraint(this=exp.to_identifier(constraint.name), expression=unique_constraint)
95
+ return unique_constraint
96
+
97
+ if constraint.constraint_type == "CHECK":
98
+ check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
99
+
100
+ if constraint.name:
101
+ return exp.Constraint(this=exp.to_identifier(constraint.name), expression=check_expr)
102
+ return check_expr
103
+
104
+ return None
@@ -0,0 +1,77 @@
1
+ """Safe SQL query builder with validation and parameter binding.
2
+
3
+ This module provides a fluent interface for building SQL queries safely,
4
+ with automatic parameter binding and validation.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Optional
9
+
10
+ from sqlglot import exp
11
+
12
+ from sqlspec.builder._base import QueryBuilder, SafeQuery
13
+ from sqlspec.builder.mixins import DeleteFromClauseMixin, ReturningClauseMixin, WhereClauseMixin
14
+ from sqlspec.core.result import SQLResult
15
+
16
+ __all__ = ("Delete",)
17
+
18
+
19
+ @dataclass(unsafe_hash=True)
20
+ class Delete(QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin):
21
+ """Builder for DELETE statements.
22
+
23
+ This builder provides a fluent interface for constructing SQL DELETE statements
24
+ with automatic parameter binding and validation. It does not support JOIN
25
+ operations to maintain cross-dialect compatibility and safety.
26
+ """
27
+
28
+ _table: "Optional[str]" = field(default=None, init=False)
29
+
30
+ def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
31
+ """Initialize DELETE with optional table.
32
+
33
+ Args:
34
+ table: Target table name
35
+ **kwargs: Additional QueryBuilder arguments
36
+ """
37
+ super().__init__(**kwargs)
38
+
39
+ self._table = None
40
+
41
+ if table:
42
+ self.from_(table)
43
+
44
+ @property
45
+ def _expected_result_type(self) -> "type[SQLResult]":
46
+ """Get the expected result type for DELETE operations.
47
+
48
+ Returns:
49
+ The ExecuteResult type for DELETE statements.
50
+ """
51
+ return SQLResult
52
+
53
+ def _create_base_expression(self) -> "exp.Delete":
54
+ """Create a new sqlglot Delete expression.
55
+
56
+ Returns:
57
+ A new sqlglot Delete expression.
58
+ """
59
+ return exp.Delete()
60
+
61
+ def build(self) -> "SafeQuery":
62
+ """Build the DELETE query with validation.
63
+
64
+ Returns:
65
+ SafeQuery: The built query with SQL and parameters.
66
+
67
+ Raises:
68
+ SQLBuilderError: If the table is not specified.
69
+ """
70
+
71
+ if not self._table:
72
+ from sqlspec.exceptions import SQLBuilderError
73
+
74
+ msg = "DELETE requires a table to be specified. Use from() to set the table."
75
+ raise SQLBuilderError(msg)
76
+
77
+ return super().build()
@@ -0,0 +1,241 @@
1
+ """Safe SQL query builder with validation and parameter binding.
2
+
3
+ This module provides a fluent interface for building SQL queries safely,
4
+ with automatic parameter binding and validation.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+
10
+ from sqlglot import exp
11
+ from typing_extensions import Self
12
+
13
+ from sqlspec.builder._base import QueryBuilder
14
+ from sqlspec.builder.mixins import InsertFromSelectMixin, InsertIntoClauseMixin, InsertValuesMixin, ReturningClauseMixin
15
+ from sqlspec.core.result import SQLResult
16
+ from sqlspec.exceptions import SQLBuilderError
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Mapping, Sequence
20
+
21
+
22
+ __all__ = ("Insert",)
23
+
24
+ ERR_MSG_TABLE_NOT_SET = "The target table must be set using .into() before adding values."
25
+ ERR_MSG_VALUES_COLUMNS_MISMATCH = (
26
+ "Number of values ({values_len}) does not match the number of specified columns ({columns_len})."
27
+ )
28
+ ERR_MSG_INTERNAL_EXPRESSION_TYPE = "Internal error: expression is not an Insert instance as expected."
29
+ ERR_MSG_EXPRESSION_NOT_INITIALIZED = "Internal error: base expression not initialized."
30
+
31
+
32
+ @dataclass(unsafe_hash=True)
33
+ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSelectMixin, InsertIntoClauseMixin):
34
+ """Builder for INSERT statements.
35
+
36
+ This builder facilitates the construction of SQL INSERT queries
37
+ in a safe and dialect-agnostic manner with automatic parameter binding.
38
+ """
39
+
40
+ _table: "Optional[str]" = field(default=None, init=False)
41
+ _columns: list[str] = field(default_factory=list, init=False)
42
+ _values_added_count: int = field(default=0, init=False)
43
+
44
+ def __init__(self, table: Optional[str] = None, **kwargs: Any) -> None:
45
+ """Initialize INSERT with optional table.
46
+
47
+ Args:
48
+ table: Target table name
49
+ **kwargs: Additional QueryBuilder arguments
50
+ """
51
+ super().__init__(**kwargs)
52
+
53
+ self._table = None
54
+ self._columns = []
55
+ self._values_added_count = 0
56
+
57
+ if table:
58
+ self.into(table)
59
+
60
+ def _create_base_expression(self) -> exp.Insert:
61
+ """Create a base INSERT expression.
62
+
63
+ This method is called by the base QueryBuilder during initialization.
64
+
65
+ Returns:
66
+ A new sqlglot Insert expression.
67
+ """
68
+ return exp.Insert()
69
+
70
+ @property
71
+ def _expected_result_type(self) -> "type[SQLResult]":
72
+ """Specifies the expected result type for an INSERT query.
73
+
74
+ Returns:
75
+ The type of result expected for INSERT operations.
76
+ """
77
+ return SQLResult
78
+
79
+ def _get_insert_expression(self) -> exp.Insert:
80
+ """Safely gets and casts the internal expression to exp.Insert.
81
+
82
+ Returns:
83
+ The internal expression as exp.Insert.
84
+
85
+ Raises:
86
+ SQLBuilderError: If the expression is not initialized or is not an Insert.
87
+ """
88
+ if self._expression is None:
89
+ raise SQLBuilderError(ERR_MSG_EXPRESSION_NOT_INITIALIZED)
90
+ if not isinstance(self._expression, exp.Insert):
91
+ raise SQLBuilderError(ERR_MSG_INTERNAL_EXPRESSION_TYPE)
92
+ return self._expression
93
+
94
+ def values(self, *values: Any) -> "Self":
95
+ """Adds a row of values to the INSERT statement.
96
+
97
+ This method can be called multiple times to insert multiple rows,
98
+ resulting in a multi-row INSERT statement like `VALUES (...), (...)`.
99
+
100
+ Args:
101
+ *values: The values for the row to be inserted. The number of values
102
+ must match the number of columns set by `columns()`, if `columns()` was called
103
+ and specified any non-empty list of columns.
104
+
105
+ Returns:
106
+ The current builder instance for method chaining.
107
+
108
+ Raises:
109
+ SQLBuilderError: If `into()` has not been called to set the table,
110
+ or if `columns()` was called with a non-empty list of columns
111
+ and the number of values does not match the number of specified columns.
112
+ """
113
+ if not self._table:
114
+ raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
115
+
116
+ insert_expr = self._get_insert_expression()
117
+
118
+ if self._columns and len(values) != len(self._columns):
119
+ msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
120
+ raise SQLBuilderError(msg)
121
+
122
+ param_names = []
123
+ for i, value in enumerate(values):
124
+ # Try to use column name if available, otherwise use position-based name
125
+ if self._columns and i < len(self._columns):
126
+ column_name = (
127
+ str(self._columns[i]).split(".")[-1] if "." in str(self._columns[i]) else str(self._columns[i])
128
+ )
129
+ param_name = self._generate_unique_parameter_name(column_name)
130
+ else:
131
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
132
+ _, param_name = self.add_parameter(value, name=param_name)
133
+ param_names.append(param_name)
134
+ value_placeholders = tuple(exp.var(name) for name in param_names)
135
+
136
+ current_values_expression = insert_expr.args.get("expression")
137
+
138
+ if self._values_added_count == 0:
139
+ new_values_node = exp.Values(expressions=[exp.Tuple(expressions=list(value_placeholders))])
140
+ insert_expr.set("expression", new_values_node)
141
+ elif isinstance(current_values_expression, exp.Values):
142
+ current_values_expression.expressions.append(exp.Tuple(expressions=list(value_placeholders)))
143
+ else:
144
+ new_values_node = exp.Values(expressions=[exp.Tuple(expressions=list(value_placeholders))])
145
+ insert_expr.set("expression", new_values_node)
146
+
147
+ self._values_added_count += 1
148
+ return self
149
+
150
+ def values_from_dict(self, data: "Mapping[str, Any]") -> "Self":
151
+ """Adds a row of values from a dictionary.
152
+
153
+ This is a convenience method that automatically sets columns based on
154
+ the dictionary keys and values based on the dictionary values.
155
+
156
+ Args:
157
+ data: A mapping of column names to values.
158
+
159
+ Returns:
160
+ The current builder instance for method chaining.
161
+
162
+ Raises:
163
+ SQLBuilderError: If `into()` has not been called to set the table.
164
+ """
165
+ if not self._table:
166
+ raise SQLBuilderError(ERR_MSG_TABLE_NOT_SET)
167
+
168
+ if not self._columns:
169
+ self.columns(*data.keys())
170
+ elif set(self._columns) != set(data.keys()):
171
+ msg = f"Dictionary keys {set(data.keys())} do not match existing columns {set(self._columns)}."
172
+ raise SQLBuilderError(msg)
173
+
174
+ return self.values(*[data[col] for col in self._columns])
175
+
176
+ def values_from_dicts(self, data: "Sequence[Mapping[str, Any]]") -> "Self":
177
+ """Adds multiple rows of values from a sequence of dictionaries.
178
+
179
+ This is a convenience method for bulk inserts from structured data.
180
+
181
+ Args:
182
+ data: A sequence of mappings, each representing a row of data.
183
+
184
+ Returns:
185
+ The current builder instance for method chaining.
186
+
187
+ Raises:
188
+ SQLBuilderError: If `into()` has not been called to set the table,
189
+ or if dictionaries have inconsistent keys.
190
+ """
191
+ if not data:
192
+ return self
193
+
194
+ first_dict = data[0]
195
+ if not self._columns:
196
+ self.columns(*first_dict.keys())
197
+
198
+ expected_keys = set(self._columns)
199
+ for i, row_dict in enumerate(data):
200
+ if set(row_dict.keys()) != expected_keys:
201
+ msg = (
202
+ f"Dictionary at index {i} has keys {set(row_dict.keys())} "
203
+ f"which do not match expected keys {expected_keys}."
204
+ )
205
+ raise SQLBuilderError(msg)
206
+
207
+ for row_dict in data:
208
+ self.values(*[row_dict[col] for col in self._columns])
209
+
210
+ return self
211
+
212
+ def on_conflict_do_nothing(self) -> "Self":
213
+ """Adds an ON CONFLICT DO NOTHING clause (PostgreSQL syntax).
214
+
215
+ This is used to ignore rows that would cause a conflict.
216
+
217
+ Returns:
218
+ The current builder instance for method chaining.
219
+
220
+ Note:
221
+ This is PostgreSQL-specific syntax. Different databases have different syntax.
222
+ For a more general solution, you might need dialect-specific handling.
223
+ """
224
+ insert_expr = self._get_insert_expression()
225
+ try:
226
+ on_conflict = exp.OnConflict(this=None, expressions=[])
227
+ insert_expr.set("on", on_conflict)
228
+ except AttributeError:
229
+ pass
230
+ return self
231
+
232
+ def on_duplicate_key_update(self, **set_values: Any) -> "Self":
233
+ """Adds an ON DUPLICATE KEY UPDATE clause (MySQL syntax).
234
+
235
+ Args:
236
+ **set_values: Column-value pairs to update on duplicate key.
237
+
238
+ Returns:
239
+ The current builder instance for method chaining.
240
+ """
241
+ return self
@@ -0,0 +1,56 @@
1
+ """Safe SQL query builder with validation and parameter binding.
2
+
3
+ This module provides a fluent interface for building SQL queries safely,
4
+ with automatic parameter binding and validation.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from sqlglot import exp
10
+
11
+ from sqlspec.builder._base import QueryBuilder
12
+ from sqlspec.builder.mixins import (
13
+ MergeIntoClauseMixin,
14
+ MergeMatchedClauseMixin,
15
+ MergeNotMatchedBySourceClauseMixin,
16
+ MergeNotMatchedClauseMixin,
17
+ MergeOnClauseMixin,
18
+ MergeUsingClauseMixin,
19
+ )
20
+ from sqlspec.core.result import SQLResult
21
+
22
+ __all__ = ("Merge",)
23
+
24
+
25
+ @dataclass(unsafe_hash=True)
26
+ class Merge(
27
+ QueryBuilder,
28
+ MergeUsingClauseMixin,
29
+ MergeOnClauseMixin,
30
+ MergeMatchedClauseMixin,
31
+ MergeNotMatchedClauseMixin,
32
+ MergeIntoClauseMixin,
33
+ MergeNotMatchedBySourceClauseMixin,
34
+ ):
35
+ """Builder for MERGE statements.
36
+
37
+ This builder provides a fluent interface for constructing SQL MERGE statements
38
+ (also known as UPSERT in some databases) with automatic parameter binding and validation.
39
+ """
40
+
41
+ @property
42
+ def _expected_result_type(self) -> "type[SQLResult]":
43
+ """Return the expected result type for this builder.
44
+
45
+ Returns:
46
+ The SQLResult type for MERGE statements.
47
+ """
48
+ return SQLResult
49
+
50
+ def _create_base_expression(self) -> "exp.Merge":
51
+ """Create a base MERGE expression.
52
+
53
+ Returns:
54
+ A new sqlglot Merge expression with empty clauses.
55
+ """
56
+ return exp.Merge(this=None, using=None, on=None, whens=exp.Whens(expressions=[]))
@@ -0,0 +1,140 @@
1
+ """Centralized parsing utilities for SQLSpec builders.
2
+
3
+ This module provides common parsing functions to handle complex SQL expressions
4
+ that users might pass as strings to various builder methods.
5
+ """
6
+
7
+ import contextlib
8
+ from typing import Any, Optional, Union, cast
9
+
10
+ from sqlglot import exp, maybe_parse, parse_one
11
+
12
+ from sqlspec.utils.type_guards import has_expression_attr, has_parameter_builder
13
+
14
+
15
+ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> exp.Expression:
16
+ """Parse a column input that might be a complex expression.
17
+
18
+ Handles cases like:
19
+ - Simple column names: "name" -> Column(this=name)
20
+ - Qualified names: "users.name" -> Column(table=users, this=name)
21
+ - Aliased columns: "name AS user_name" -> Alias(this=Column(name), alias=user_name)
22
+ - Function calls: "MAX(price)" -> Max(this=Column(price))
23
+ - Complex expressions: "CASE WHEN ... END" -> Case(...)
24
+ - Custom Column objects from our builder
25
+
26
+ Args:
27
+ column_input: String, SQLGlot expression, or Column object
28
+
29
+ Returns:
30
+ exp.Expression: Parsed SQLGlot expression
31
+ """
32
+ if isinstance(column_input, exp.Expression):
33
+ return column_input
34
+
35
+ if has_expression_attr(column_input):
36
+ attr_value = getattr(column_input, "_expression", None)
37
+ if isinstance(attr_value, exp.Expression):
38
+ return attr_value
39
+
40
+ return exp.maybe_parse(column_input) or exp.column(str(column_input))
41
+
42
+
43
+ def parse_table_expression(table_input: str, explicit_alias: Optional[str] = None) -> exp.Expression:
44
+ """Parses a table string that can be a name, a name with an alias, or a subquery string."""
45
+ with contextlib.suppress(Exception):
46
+ parsed = parse_one(f"SELECT * FROM {table_input}")
47
+ if isinstance(parsed, exp.Select) and parsed.args.get("from"):
48
+ from_clause = cast("exp.From", parsed.args.get("from"))
49
+ table_expr = from_clause.this
50
+
51
+ if explicit_alias:
52
+ return exp.alias_(table_expr, explicit_alias) # type:ignore[no-any-return]
53
+ return table_expr # type:ignore[no-any-return]
54
+
55
+ return exp.to_table(table_input, alias=explicit_alias)
56
+
57
+
58
+ def parse_order_expression(order_input: Union[str, exp.Expression]) -> exp.Expression:
59
+ """Parse an ORDER BY expression that might include direction.
60
+
61
+ Handles cases like:
62
+ - Simple column: "name" -> Column(this=name)
63
+ - With direction: "name DESC" -> Ordered(this=Column(name), desc=True)
64
+ - Qualified: "users.name ASC" -> Ordered(this=Column(table=users, this=name), desc=False)
65
+ - Function: "COUNT(*) DESC" -> Ordered(this=Count(this=Star), desc=True)
66
+
67
+ Args:
68
+ order_input: String or SQLGlot expression for ORDER BY
69
+
70
+ Returns:
71
+ exp.Expression: Parsed SQLGlot expression (usually Ordered or Column)
72
+ """
73
+ if isinstance(order_input, exp.Expression):
74
+ return order_input
75
+
76
+ with contextlib.suppress(Exception):
77
+ parsed = maybe_parse(str(order_input), into=exp.Ordered)
78
+ if parsed:
79
+ return parsed
80
+
81
+ return parse_column_expression(order_input)
82
+
83
+
84
+ def parse_condition_expression(
85
+ condition_input: Union[str, exp.Expression, tuple[str, Any]], builder: "Any" = None
86
+ ) -> exp.Expression:
87
+ """Parse a condition that might be complex SQL.
88
+
89
+ Handles cases like:
90
+ - Simple conditions: "name = 'John'" -> EQ(Column(name), Literal('John'))
91
+ - Tuple format: ("name", "John") -> EQ(Column(name), Literal('John'))
92
+ - Complex conditions: "age > 18 AND status = 'active'" -> And(GT(...), EQ(...))
93
+ - Function conditions: "LENGTH(name) > 5" -> GT(Length(Column(name)), Literal(5))
94
+
95
+ Args:
96
+ condition_input: String, tuple, or SQLGlot expression for condition
97
+ builder: Optional builder instance for parameter binding
98
+
99
+ Returns:
100
+ exp.Expression: Parsed SQLGlot expression (usually a comparison or logical op)
101
+ """
102
+ if isinstance(condition_input, exp.Expression):
103
+ return condition_input
104
+
105
+ tuple_condition_parts = 2
106
+ if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
107
+ column, value = condition_input
108
+ column_expr = parse_column_expression(column)
109
+ if value is None:
110
+ return exp.Is(this=column_expr, expression=exp.null())
111
+ if builder and has_parameter_builder(builder):
112
+ from sqlspec.builder.mixins._where_clause import _extract_column_name
113
+
114
+ column_name = _extract_column_name(column)
115
+ param_name = builder._generate_unique_parameter_name(column_name)
116
+ _, param_name = builder.add_parameter(value, name=param_name)
117
+ return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
118
+ if isinstance(value, str):
119
+ return exp.EQ(this=column_expr, expression=exp.convert(value))
120
+ if isinstance(value, (int, float)):
121
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
122
+ return exp.EQ(this=column_expr, expression=exp.convert(str(value)))
123
+
124
+ if not isinstance(condition_input, str):
125
+ condition_input = str(condition_input)
126
+
127
+ try:
128
+ return exp.condition(condition_input)
129
+ except Exception:
130
+ try:
131
+ parsed = exp.maybe_parse(condition_input) # type: ignore[var-annotated]
132
+ if parsed:
133
+ return parsed # type:ignore[no-any-return]
134
+ except Exception: # noqa: S110
135
+ pass
136
+
137
+ return exp.condition(condition_input)
138
+
139
+
140
+ __all__ = ("parse_column_expression", "parse_condition_expression", "parse_order_expression", "parse_table_expression")