sqlspec 0.12.2__py3-none-any.whl → 0.13.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 (113) hide show
  1. sqlspec/_sql.py +21 -180
  2. sqlspec/adapters/adbc/config.py +10 -12
  3. sqlspec/adapters/adbc/driver.py +120 -118
  4. sqlspec/adapters/aiosqlite/config.py +3 -3
  5. sqlspec/adapters/aiosqlite/driver.py +100 -130
  6. sqlspec/adapters/asyncmy/config.py +3 -4
  7. sqlspec/adapters/asyncmy/driver.py +123 -135
  8. sqlspec/adapters/asyncpg/config.py +3 -7
  9. sqlspec/adapters/asyncpg/driver.py +98 -140
  10. sqlspec/adapters/bigquery/config.py +4 -5
  11. sqlspec/adapters/bigquery/driver.py +125 -167
  12. sqlspec/adapters/duckdb/config.py +3 -6
  13. sqlspec/adapters/duckdb/driver.py +114 -111
  14. sqlspec/adapters/oracledb/config.py +6 -5
  15. sqlspec/adapters/oracledb/driver.py +242 -259
  16. sqlspec/adapters/psqlpy/config.py +3 -7
  17. sqlspec/adapters/psqlpy/driver.py +118 -93
  18. sqlspec/adapters/psycopg/config.py +18 -31
  19. sqlspec/adapters/psycopg/driver.py +283 -236
  20. sqlspec/adapters/sqlite/config.py +3 -3
  21. sqlspec/adapters/sqlite/driver.py +103 -97
  22. sqlspec/config.py +0 -4
  23. sqlspec/driver/_async.py +89 -98
  24. sqlspec/driver/_common.py +52 -17
  25. sqlspec/driver/_sync.py +81 -105
  26. sqlspec/driver/connection.py +207 -0
  27. sqlspec/driver/mixins/_csv_writer.py +91 -0
  28. sqlspec/driver/mixins/_pipeline.py +38 -49
  29. sqlspec/driver/mixins/_result_utils.py +27 -9
  30. sqlspec/driver/mixins/_storage.py +67 -181
  31. sqlspec/driver/mixins/_type_coercion.py +3 -4
  32. sqlspec/driver/parameters.py +138 -0
  33. sqlspec/exceptions.py +10 -2
  34. sqlspec/extensions/aiosql/adapter.py +0 -10
  35. sqlspec/extensions/litestar/handlers.py +0 -1
  36. sqlspec/extensions/litestar/plugin.py +0 -3
  37. sqlspec/extensions/litestar/providers.py +0 -14
  38. sqlspec/loader.py +25 -90
  39. sqlspec/protocols.py +542 -0
  40. sqlspec/service/__init__.py +3 -2
  41. sqlspec/service/_util.py +147 -0
  42. sqlspec/service/base.py +1116 -9
  43. sqlspec/statement/builder/__init__.py +42 -32
  44. sqlspec/statement/builder/_ddl_utils.py +0 -10
  45. sqlspec/statement/builder/_parsing_utils.py +10 -4
  46. sqlspec/statement/builder/base.py +67 -22
  47. sqlspec/statement/builder/column.py +283 -0
  48. sqlspec/statement/builder/ddl.py +91 -67
  49. sqlspec/statement/builder/delete.py +23 -7
  50. sqlspec/statement/builder/insert.py +29 -15
  51. sqlspec/statement/builder/merge.py +4 -4
  52. sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
  53. sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
  54. sqlspec/statement/builder/mixins/_delete_from.py +1 -1
  55. sqlspec/statement/builder/mixins/_from.py +10 -8
  56. sqlspec/statement/builder/mixins/_group_by.py +0 -1
  57. sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
  58. sqlspec/statement/builder/mixins/_insert_values.py +0 -2
  59. sqlspec/statement/builder/mixins/_join.py +20 -13
  60. sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
  61. sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
  62. sqlspec/statement/builder/mixins/_order_by.py +2 -2
  63. sqlspec/statement/builder/mixins/_pivot.py +4 -7
  64. sqlspec/statement/builder/mixins/_select_columns.py +6 -5
  65. sqlspec/statement/builder/mixins/_unpivot.py +6 -9
  66. sqlspec/statement/builder/mixins/_update_from.py +2 -1
  67. sqlspec/statement/builder/mixins/_update_set.py +11 -8
  68. sqlspec/statement/builder/mixins/_where.py +61 -34
  69. sqlspec/statement/builder/select.py +32 -17
  70. sqlspec/statement/builder/update.py +25 -11
  71. sqlspec/statement/filters.py +39 -14
  72. sqlspec/statement/parameter_manager.py +220 -0
  73. sqlspec/statement/parameters.py +210 -79
  74. sqlspec/statement/pipelines/__init__.py +166 -23
  75. sqlspec/statement/pipelines/analyzers/_analyzer.py +21 -20
  76. sqlspec/statement/pipelines/context.py +35 -39
  77. sqlspec/statement/pipelines/transformers/__init__.py +2 -3
  78. sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
  79. sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +628 -58
  80. sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
  81. sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
  82. sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
  83. sqlspec/statement/pipelines/validators/_performance.py +38 -23
  84. sqlspec/statement/pipelines/validators/_security.py +39 -62
  85. sqlspec/statement/result.py +37 -129
  86. sqlspec/statement/splitter.py +0 -12
  87. sqlspec/statement/sql.py +863 -391
  88. sqlspec/statement/sql_compiler.py +140 -0
  89. sqlspec/storage/__init__.py +10 -2
  90. sqlspec/storage/backends/fsspec.py +53 -8
  91. sqlspec/storage/backends/obstore.py +15 -19
  92. sqlspec/storage/capabilities.py +101 -0
  93. sqlspec/storage/registry.py +56 -83
  94. sqlspec/typing.py +6 -434
  95. sqlspec/utils/cached_property.py +25 -0
  96. sqlspec/utils/correlation.py +0 -2
  97. sqlspec/utils/logging.py +0 -6
  98. sqlspec/utils/sync_tools.py +0 -4
  99. sqlspec/utils/text.py +0 -5
  100. sqlspec/utils/type_guards.py +892 -0
  101. {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/METADATA +1 -1
  102. sqlspec-0.13.0.dist-info/RECORD +150 -0
  103. sqlspec/statement/builder/protocols.py +0 -20
  104. sqlspec/statement/pipelines/base.py +0 -315
  105. sqlspec/statement/pipelines/result_types.py +0 -41
  106. sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
  107. sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
  108. sqlspec/statement/pipelines/validators/base.py +0 -67
  109. sqlspec/storage/protocol.py +0 -173
  110. sqlspec-0.12.2.dist-info/RECORD +0 -145
  111. {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/WHEEL +0 -0
  112. {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/licenses/LICENSE +0 -0
  113. {sqlspec-0.12.2.dist-info → sqlspec-0.13.0.dist-info}/licenses/NOTICE +0 -0
@@ -8,47 +8,57 @@ parameter binding and validation.
8
8
 
9
9
  from sqlspec.exceptions import SQLBuilderError
10
10
  from sqlspec.statement.builder.base import QueryBuilder, SafeQuery
11
+ from sqlspec.statement.builder.column import Column, ColumnExpression, FunctionColumn
11
12
  from sqlspec.statement.builder.ddl import (
12
- AlterTableBuilder,
13
- CreateIndexBuilder,
14
- CreateMaterializedViewBuilder,
15
- CreateSchemaBuilder,
16
- CreateTableAsSelectBuilder,
17
- CreateViewBuilder,
13
+ AlterTable,
14
+ CommentOn,
15
+ CreateIndex,
16
+ CreateMaterializedView,
17
+ CreateSchema,
18
+ CreateTable,
19
+ CreateTableAsSelect,
20
+ CreateView,
18
21
  DDLBuilder,
19
- DropIndexBuilder,
20
- DropSchemaBuilder,
21
- DropTableBuilder,
22
- DropViewBuilder,
23
- TruncateTableBuilder,
22
+ DropIndex,
23
+ DropSchema,
24
+ DropTable,
25
+ DropView,
26
+ RenameTable,
27
+ TruncateTable,
24
28
  )
25
- from sqlspec.statement.builder.delete import DeleteBuilder
26
- from sqlspec.statement.builder.insert import InsertBuilder
27
- from sqlspec.statement.builder.merge import MergeBuilder
29
+ from sqlspec.statement.builder.delete import Delete
30
+ from sqlspec.statement.builder.insert import Insert
31
+ from sqlspec.statement.builder.merge import Merge
28
32
  from sqlspec.statement.builder.mixins import WhereClauseMixin
29
- from sqlspec.statement.builder.select import SelectBuilder
30
- from sqlspec.statement.builder.update import UpdateBuilder
33
+ from sqlspec.statement.builder.select import Select
34
+ from sqlspec.statement.builder.update import Update
31
35
 
32
36
  __all__ = (
33
- "AlterTableBuilder",
34
- "CreateIndexBuilder",
35
- "CreateMaterializedViewBuilder",
36
- "CreateSchemaBuilder",
37
- "CreateTableAsSelectBuilder",
38
- "CreateViewBuilder",
37
+ "AlterTable",
38
+ "Column",
39
+ "ColumnExpression",
40
+ "CommentOn",
41
+ "CreateIndex",
42
+ "CreateMaterializedView",
43
+ "CreateSchema",
44
+ "CreateTable",
45
+ "CreateTableAsSelect",
46
+ "CreateView",
39
47
  "DDLBuilder",
40
- "DeleteBuilder",
41
- "DropIndexBuilder",
42
- "DropSchemaBuilder",
43
- "DropTableBuilder",
44
- "DropViewBuilder",
45
- "InsertBuilder",
46
- "MergeBuilder",
48
+ "Delete",
49
+ "DropIndex",
50
+ "DropSchema",
51
+ "DropTable",
52
+ "DropView",
53
+ "FunctionColumn",
54
+ "Insert",
55
+ "Merge",
47
56
  "QueryBuilder",
57
+ "RenameTable",
48
58
  "SQLBuilderError",
49
59
  "SafeQuery",
50
- "SelectBuilder",
51
- "TruncateTableBuilder",
52
- "UpdateBuilder",
60
+ "Select",
61
+ "TruncateTable",
62
+ "Update",
53
63
  "WhereClauseMixin",
54
64
  )
@@ -12,10 +12,8 @@ __all__ = ("build_column_expression", "build_constraint_expression")
12
12
 
13
13
  def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
14
14
  """Build SQLGlot expression for a column definition."""
15
- # Start with column name and type
16
15
  col_def = exp.ColumnDef(this=exp.to_identifier(col.name), kind=exp.DataType.build(col.dtype))
17
16
 
18
- # Add constraints
19
17
  constraints: list[exp.ColumnConstraint] = []
20
18
 
21
19
  if col.not_null:
@@ -28,10 +26,8 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
28
26
  constraints.append(exp.ColumnConstraint(kind=exp.UniqueColumnConstraint()))
29
27
 
30
28
  if col.default is not None:
31
- # Handle different default value types
32
29
  default_expr: Optional[exp.Expression] = None
33
30
  if isinstance(col.default, str):
34
- # Check if it's a function/expression or a literal string
35
31
  if col.default.upper() in {"CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"} or "(" in col.default:
36
32
  default_expr = exp.maybe_parse(col.default)
37
33
  else:
@@ -55,14 +51,12 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
55
51
  constraints.append(exp.ColumnConstraint(kind=exp.CommentColumnConstraint(this=exp.Literal.string(col.comment))))
56
52
 
57
53
  if col.generated:
58
- # Handle generated columns (computed columns)
59
54
  generated_expr = exp.GeneratedAsIdentityColumnConstraint(this=exp.maybe_parse(col.generated))
60
55
  constraints.append(exp.ColumnConstraint(kind=generated_expr))
61
56
 
62
57
  if col.collate:
63
58
  constraints.append(exp.ColumnConstraint(kind=exp.CollateColumnConstraint(this=exp.to_identifier(col.collate))))
64
59
 
65
- # Set constraints on column definition
66
60
  if constraints:
67
61
  col_def.set("constraints", constraints)
68
62
 
@@ -72,7 +66,6 @@ def build_column_expression(col: "ColumnDefinition") -> "exp.Expression":
72
66
  def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional[exp.Expression]":
73
67
  """Build SQLGlot expression for a table constraint."""
74
68
  if constraint.constraint_type == "PRIMARY KEY":
75
- # Build primary key constraint
76
69
  pk_cols = [exp.to_identifier(col) for col in constraint.columns]
77
70
  pk_constraint = exp.PrimaryKey(expressions=pk_cols)
78
71
 
@@ -81,7 +74,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
81
74
  return pk_constraint
82
75
 
83
76
  if constraint.constraint_type == "FOREIGN KEY":
84
- # Build foreign key constraint
85
77
  fk_cols = [exp.to_identifier(col) for col in constraint.columns]
86
78
  ref_cols = [exp.to_identifier(col) for col in constraint.references_columns]
87
79
 
@@ -100,7 +92,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
100
92
  return fk_constraint
101
93
 
102
94
  if constraint.constraint_type == "UNIQUE":
103
- # Build unique constraint
104
95
  unique_cols = [exp.to_identifier(col) for col in constraint.columns]
105
96
  unique_constraint = exp.UniqueKeyProperty(expressions=unique_cols)
106
97
 
@@ -109,7 +100,6 @@ def build_constraint_expression(constraint: "ConstraintDefinition") -> "Optional
109
100
  return unique_constraint
110
101
 
111
102
  if constraint.constraint_type == "CHECK":
112
- # Build check constraint
113
103
  check_expr = exp.Check(this=exp.maybe_parse(constraint.condition) if constraint.condition else None)
114
104
 
115
105
  if constraint.name:
@@ -10,7 +10,7 @@ from typing import Any, Optional, Union, cast
10
10
  from sqlglot import exp, maybe_parse, parse_one
11
11
 
12
12
 
13
- def parse_column_expression(column_input: Union[str, exp.Expression]) -> exp.Expression:
13
+ def parse_column_expression(column_input: Union[str, exp.Expression, Any]) -> exp.Expression:
14
14
  """Parse a column input that might be a complex expression.
15
15
 
16
16
  Handles cases like:
@@ -19,15 +19,23 @@ def parse_column_expression(column_input: Union[str, exp.Expression]) -> exp.Exp
19
19
  - Aliased columns: "name AS user_name" -> Alias(this=Column(name), alias=user_name)
20
20
  - Function calls: "MAX(price)" -> Max(this=Column(price))
21
21
  - Complex expressions: "CASE WHEN ... END" -> Case(...)
22
+ - Custom Column objects from our builder
22
23
 
23
24
  Args:
24
- column_input: String or SQLGlot expression representing a column/expression
25
+ column_input: String, SQLGlot expression, or Column object
25
26
 
26
27
  Returns:
27
28
  exp.Expression: Parsed SQLGlot expression
28
29
  """
29
30
  if isinstance(column_input, exp.Expression):
30
31
  return column_input
32
+
33
+ # Handle our custom Column objects
34
+ if hasattr(column_input, "_expr"):
35
+ attr_value = getattr(column_input, "_expr", None)
36
+ if isinstance(attr_value, exp.Expression):
37
+ return attr_value
38
+
31
39
  return exp.maybe_parse(column_input) or exp.column(str(column_input))
32
40
 
33
41
 
@@ -96,7 +104,6 @@ def parse_condition_expression(
96
104
 
97
105
  tuple_condition_parts = 2
98
106
  if isinstance(condition_input, tuple) and len(condition_input) == tuple_condition_parts:
99
- # Handle (column, value) tuple format with proper parameter binding
100
107
  column, value = condition_input
101
108
  column_expr = parse_column_expression(column)
102
109
  if value is None:
@@ -105,7 +112,6 @@ def parse_condition_expression(
105
112
  if builder and hasattr(builder, "add_parameter"):
106
113
  _, param_name = builder.add_parameter(value)
107
114
  return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
108
- # Fallback to literal value
109
115
  if isinstance(value, str):
110
116
  return exp.EQ(this=column_expr, expression=exp.Literal.string(value))
111
117
  if isinstance(value, (int, float)):
@@ -7,7 +7,7 @@ advanced builder patterns and optimization capabilities.
7
7
 
8
8
  from abc import ABC, abstractmethod
9
9
  from dataclasses import dataclass, field
10
- from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union
10
+ from typing import TYPE_CHECKING, Any, Generic, NoReturn, Optional, Union, cast
11
11
 
12
12
  import sqlglot
13
13
  from sqlglot import Dialect, exp
@@ -20,6 +20,7 @@ from sqlspec.exceptions import SQLBuilderError
20
20
  from sqlspec.statement.sql import SQL, SQLConfig
21
21
  from sqlspec.typing import RowT
22
22
  from sqlspec.utils.logging import get_logger
23
+ from sqlspec.utils.type_guards import has_sql_method, has_with_method
23
24
 
24
25
  if TYPE_CHECKING:
25
26
  from sqlspec.statement.result import SQLResult
@@ -124,6 +125,32 @@ class QueryBuilder(ABC, Generic[RowT]):
124
125
  self._parameters[param_name] = value
125
126
  return param_name
126
127
 
128
+ def _parameterize_expression(self, expression: exp.Expression) -> exp.Expression:
129
+ """Replace literal values in an expression with bound parameters.
130
+
131
+ This method traverses a SQLGlot expression tree and replaces literal
132
+ values with parameter placeholders, adding the values to the builder's
133
+ parameter collection.
134
+
135
+ Args:
136
+ expression: The SQLGlot expression to parameterize
137
+
138
+ Returns:
139
+ A new expression with literals replaced by parameter placeholders
140
+ """
141
+
142
+ def replacer(node: exp.Expression) -> exp.Expression:
143
+ if isinstance(node, exp.Literal):
144
+ # Skip boolean literals (TRUE/FALSE) and NULL
145
+ if node.this in (True, False, None):
146
+ return node
147
+ # Convert other literals to parameters
148
+ param_name = self._add_parameter(node.this, context="where")
149
+ return exp.Placeholder(this=param_name)
150
+ return node
151
+
152
+ return expression.transform(replacer, copy=True)
153
+
127
154
  def add_parameter(self: Self, value: Any, name: Optional[str] = None) -> tuple[Self, str]:
128
155
  """Explicitly adds a parameter to the query.
129
156
 
@@ -191,7 +218,7 @@ class QueryBuilder(ABC, Generic[RowT]):
191
218
  msg = f"CTE query builder expression must be a Select, got {type(query._expression).__name__}."
192
219
  self._raise_sql_builder_error(msg)
193
220
  cte_select_expression = query._expression.copy()
194
- for p_name, p_value in query._parameters.items():
221
+ for p_name, p_value in query.parameters.items():
195
222
  # Try to preserve original parameter name, only rename if collision
196
223
  unique_name = self._generate_unique_parameter_name(p_name)
197
224
  self.add_parameter(p_value, unique_name)
@@ -231,23 +258,21 @@ class QueryBuilder(ABC, Generic[RowT]):
231
258
  final_expression = self._expression.copy()
232
259
 
233
260
  if self._with_ctes:
234
- if hasattr(final_expression, "with_") and callable(getattr(final_expression, "with_", None)):
261
+ if has_with_method(final_expression):
262
+ # Type checker now knows final_expression has with_ method
235
263
  for alias, cte_node in self._with_ctes.items():
236
- final_expression = final_expression.with_( # pyright: ignore
237
- cte_node.args["this"], as_=alias, copy=False
238
- )
239
- elif (
240
- isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union))
241
- and self._with_ctes
242
- ):
264
+ final_expression = cast("Any", final_expression).with_(cte_node.args["this"], as_=alias, copy=False)
265
+ elif isinstance(final_expression, (exp.Select, exp.Insert, exp.Update, exp.Delete, exp.Union)):
243
266
  final_expression = exp.With(expressions=list(self._with_ctes.values()), this=final_expression)
244
267
 
245
- # Apply SQLGlot optimizations if enabled
246
- if self.enable_optimization:
268
+ if self.enable_optimization and isinstance(final_expression, exp.Expression):
247
269
  final_expression = self._optimize_expression(final_expression)
248
270
 
249
271
  try:
250
- sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True)
272
+ if has_sql_method(final_expression):
273
+ sql_string = final_expression.sql(dialect=self.dialect_name, pretty=True) # pyright: ignore[reportAttributeAccessIssue]
274
+ else:
275
+ sql_string = str(final_expression)
251
276
  except Exception as e:
252
277
  err_msg = f"Error generating SQL from expression: {e!s}"
253
278
  logger.exception("SQL generation failed")
@@ -294,13 +319,30 @@ class QueryBuilder(ABC, Generic[RowT]):
294
319
  """
295
320
  safe_query = self.build()
296
321
 
297
- return SQL(
298
- statement=safe_query.sql,
299
- parameters=safe_query.parameters,
300
- _dialect=safe_query.dialect,
301
- _config=config,
302
- _builder_result_type=self._expected_result_type,
303
- )
322
+ if isinstance(safe_query.parameters, dict):
323
+ kwargs = safe_query.parameters
324
+ parameters = None
325
+ else:
326
+ kwargs = None
327
+ parameters = (
328
+ safe_query.parameters
329
+ if isinstance(safe_query.parameters, tuple)
330
+ else tuple(safe_query.parameters)
331
+ if safe_query.parameters
332
+ else None
333
+ )
334
+
335
+ if config is None:
336
+ from sqlspec.statement.sql import SQLConfig
337
+
338
+ config = SQLConfig(dialect=safe_query.dialect)
339
+
340
+ # SQL expects parameters as variadic args, not as a keyword
341
+ if kwargs:
342
+ return SQL(safe_query.sql, config=config, **kwargs)
343
+ if parameters:
344
+ return SQL(safe_query.sql, *parameters, config=config)
345
+ return SQL(safe_query.sql, config=config)
304
346
 
305
347
  def __str__(self) -> str:
306
348
  """Return the SQL string representation of the query.
@@ -311,7 +353,6 @@ class QueryBuilder(ABC, Generic[RowT]):
311
353
  try:
312
354
  return self.build().sql
313
355
  except Exception:
314
- # Fallback to default representation if build fails
315
356
  return super().__str__()
316
357
 
317
358
  @property
@@ -324,7 +365,11 @@ class QueryBuilder(ABC, Generic[RowT]):
324
365
  return self.dialect.__name__.lower()
325
366
  if isinstance(self.dialect, Dialect):
326
367
  return type(self.dialect).__name__.lower()
327
- # Handle case where dialect might have a __name__ attribute
328
368
  if hasattr(self.dialect, "__name__"):
329
369
  return self.dialect.__name__.lower()
330
370
  return None
371
+
372
+ @property
373
+ def parameters(self) -> dict[str, Any]:
374
+ """Public access to query parameters."""
375
+ return self._parameters
@@ -0,0 +1,283 @@
1
+ """Pythonic column expressions for query building.
2
+
3
+ This module provides Column objects that support native Python operators
4
+ for building SQL conditions with type safety and parameter binding.
5
+ """
6
+
7
+ from collections.abc import Iterable
8
+ from typing import Any, Optional
9
+
10
+ from sqlglot import exp
11
+
12
+ from sqlspec.utils.type_guards import has_sql_method
13
+
14
+ __all__ = ("Column", "ColumnExpression", "FunctionColumn")
15
+
16
+
17
+ class ColumnExpression:
18
+ """Base class for column expressions that can be combined with operators."""
19
+
20
+ def __init__(self, expression: exp.Expression) -> None:
21
+ self._expr = expression
22
+
23
+ def __and__(self, other: "ColumnExpression") -> "ColumnExpression":
24
+ """Combine with AND operator (&)."""
25
+ if not isinstance(other, ColumnExpression):
26
+ return NotImplemented
27
+ return ColumnExpression(exp.And(this=self._expr, expression=other._expr))
28
+
29
+ def __or__(self, other: "ColumnExpression") -> "ColumnExpression":
30
+ """Combine with OR operator (|)."""
31
+ if not isinstance(other, ColumnExpression):
32
+ return NotImplemented
33
+ return ColumnExpression(exp.Or(this=self._expr, expression=other._expr))
34
+
35
+ def __invert__(self) -> "ColumnExpression":
36
+ """Apply NOT operator (~)."""
37
+ return ColumnExpression(exp.Not(this=self._expr))
38
+
39
+ def __bool__(self) -> bool:
40
+ """Prevent accidental use of 'and'/'or' keywords."""
41
+ msg = (
42
+ "Cannot use 'and'/'or' operators on ColumnExpression. "
43
+ "Use '&'/'|' operators instead. "
44
+ f"Expression: {self._expr.sql()}"
45
+ )
46
+ raise TypeError(msg)
47
+
48
+ @property
49
+ def sqlglot_expression(self) -> exp.Expression:
50
+ """Get the underlying SQLGlot expression."""
51
+ return self._expr
52
+
53
+
54
+ class Column:
55
+ """Represents a database column with Python operator support."""
56
+
57
+ def __init__(self, name: str, table: Optional[str] = None) -> None:
58
+ self.name = name
59
+ self.table = table
60
+
61
+ # Create SQLGlot column expression
62
+ if table:
63
+ self._expr = exp.Column(this=exp.Identifier(this=name), table=exp.Identifier(this=table))
64
+ else:
65
+ self._expr = exp.Column(this=exp.Identifier(this=name))
66
+
67
+ # Comparison operators
68
+ def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
69
+ """Equal to (==)."""
70
+ if other is None:
71
+ return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
72
+ return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
73
+
74
+ def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
75
+ """Not equal to (!=)."""
76
+ if other is None:
77
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
78
+ return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
79
+
80
+ def __gt__(self, other: Any) -> ColumnExpression:
81
+ """Greater than (>)."""
82
+ return ColumnExpression(exp.GT(this=self._expr, expression=exp.convert(other)))
83
+
84
+ def __ge__(self, other: Any) -> ColumnExpression:
85
+ """Greater than or equal (>=)."""
86
+ return ColumnExpression(exp.GTE(this=self._expr, expression=exp.convert(other)))
87
+
88
+ def __lt__(self, other: Any) -> ColumnExpression:
89
+ """Less than (<)."""
90
+ return ColumnExpression(exp.LT(this=self._expr, expression=exp.convert(other)))
91
+
92
+ def __le__(self, other: Any) -> ColumnExpression:
93
+ """Less than or equal (<=)."""
94
+ return ColumnExpression(exp.LTE(this=self._expr, expression=exp.convert(other)))
95
+
96
+ def __invert__(self) -> ColumnExpression:
97
+ """Apply NOT operator (~)."""
98
+ return ColumnExpression(exp.Not(this=self._expr))
99
+
100
+ # SQL-specific methods
101
+ def like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
102
+ """SQL LIKE pattern matching."""
103
+ if escape:
104
+ like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern), escape=exp.convert(escape))
105
+ else:
106
+ like_expr = exp.Like(this=self._expr, expression=exp.convert(pattern))
107
+ return ColumnExpression(like_expr)
108
+
109
+ def ilike(self, pattern: str) -> ColumnExpression:
110
+ """Case-insensitive LIKE."""
111
+ return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
112
+
113
+ def in_(self, values: Iterable[Any]) -> ColumnExpression:
114
+ """SQL IN clause."""
115
+ converted_values = [exp.convert(v) for v in values]
116
+ return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
117
+
118
+ def not_in(self, values: Iterable[Any]) -> ColumnExpression:
119
+ """SQL NOT IN clause."""
120
+ return ~self.in_(values)
121
+
122
+ def between(self, start: Any, end: Any) -> ColumnExpression:
123
+ """SQL BETWEEN clause."""
124
+ return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
125
+
126
+ def is_null(self) -> ColumnExpression:
127
+ """SQL IS NULL."""
128
+ return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
129
+
130
+ def is_not_null(self) -> ColumnExpression:
131
+ """SQL IS NOT NULL."""
132
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
133
+
134
+ def not_like(self, pattern: str, escape: Optional[str] = None) -> ColumnExpression:
135
+ """SQL NOT LIKE pattern matching."""
136
+ return ~self.like(pattern, escape)
137
+
138
+ def not_ilike(self, pattern: str) -> ColumnExpression:
139
+ """Case-insensitive NOT LIKE."""
140
+ return ~self.ilike(pattern)
141
+
142
+ def any_(self, values: Iterable[Any]) -> ColumnExpression:
143
+ """SQL = ANY(...) clause."""
144
+ converted_values = [exp.convert(v) for v in values]
145
+ return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
146
+
147
+ def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
148
+ """SQL <> ANY(...) clause."""
149
+ converted_values = [exp.convert(v) for v in values]
150
+ return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
151
+
152
+ # SQL Functions
153
+ def lower(self) -> "FunctionColumn":
154
+ """SQL LOWER() function."""
155
+ return FunctionColumn(exp.Lower(this=self._expr))
156
+
157
+ def upper(self) -> "FunctionColumn":
158
+ """SQL UPPER() function."""
159
+ return FunctionColumn(exp.Upper(this=self._expr))
160
+
161
+ def length(self) -> "FunctionColumn":
162
+ """SQL LENGTH() function."""
163
+ return FunctionColumn(exp.Length(this=self._expr))
164
+
165
+ def trim(self) -> "FunctionColumn":
166
+ """SQL TRIM() function."""
167
+ return FunctionColumn(exp.Trim(this=self._expr))
168
+
169
+ def abs(self) -> "FunctionColumn":
170
+ """SQL ABS() function."""
171
+ return FunctionColumn(exp.Abs(this=self._expr))
172
+
173
+ def round(self, decimals: int = 0) -> "FunctionColumn":
174
+ """SQL ROUND() function."""
175
+ if decimals == 0:
176
+ return FunctionColumn(exp.Round(this=self._expr))
177
+ return FunctionColumn(exp.Round(this=self._expr, expression=exp.Literal.number(decimals)))
178
+
179
+ def floor(self) -> "FunctionColumn":
180
+ """SQL FLOOR() function."""
181
+ return FunctionColumn(exp.Floor(this=self._expr))
182
+
183
+ def ceil(self) -> "FunctionColumn":
184
+ """SQL CEIL() function."""
185
+ return FunctionColumn(exp.Ceil(this=self._expr))
186
+
187
+ def substring(self, start: int, length: Optional[int] = None) -> "FunctionColumn":
188
+ """SQL SUBSTRING() function."""
189
+ args = [exp.Literal.number(start)]
190
+ if length is not None:
191
+ args.append(exp.Literal.number(length))
192
+ return FunctionColumn(exp.Substring(this=self._expr, expressions=args))
193
+
194
+ def coalesce(self, *values: Any) -> "FunctionColumn":
195
+ """SQL COALESCE() function."""
196
+ expressions = [self._expr] + [exp.convert(v) for v in values]
197
+ return FunctionColumn(exp.Coalesce(expressions=expressions))
198
+
199
+ def cast(self, data_type: str) -> "FunctionColumn":
200
+ """SQL CAST() function."""
201
+ return FunctionColumn(exp.Cast(this=self._expr, to=exp.DataType.build(data_type)))
202
+
203
+ def alias(self, alias_name: str) -> exp.Expression:
204
+ """Create an aliased column expression."""
205
+ return exp.Alias(this=self._expr, alias=alias_name)
206
+
207
+ def __repr__(self) -> str:
208
+ if self.table:
209
+ return f"Column<{self.table}.{self.name}>"
210
+ return f"Column<{self.name}>"
211
+
212
+ def __hash__(self) -> int:
213
+ """Hash based on table and column name."""
214
+ return hash((self.table, self.name))
215
+
216
+
217
+ class FunctionColumn:
218
+ """Represents the result of a SQL function call on a column."""
219
+
220
+ def __init__(self, expression: exp.Expression) -> None:
221
+ self._expr = expression
222
+
223
+ def __eq__(self, other: object) -> ColumnExpression: # type: ignore[override]
224
+ return ColumnExpression(exp.EQ(this=self._expr, expression=exp.convert(other)))
225
+
226
+ def __ne__(self, other: object) -> ColumnExpression: # type: ignore[override]
227
+ return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.convert(other)))
228
+
229
+ def like(self, pattern: str) -> ColumnExpression:
230
+ return ColumnExpression(exp.Like(this=self._expr, expression=exp.convert(pattern)))
231
+
232
+ def ilike(self, pattern: str) -> ColumnExpression:
233
+ """Case-insensitive LIKE."""
234
+ return ColumnExpression(exp.ILike(this=self._expr, expression=exp.convert(pattern)))
235
+
236
+ def in_(self, values: Iterable[Any]) -> ColumnExpression:
237
+ """SQL IN clause."""
238
+ converted_values = [exp.convert(v) for v in values]
239
+ return ColumnExpression(exp.In(this=self._expr, expressions=converted_values))
240
+
241
+ def not_in_(self, values: Iterable[Any]) -> ColumnExpression:
242
+ """SQL NOT IN clause."""
243
+ return ~self.in_(values)
244
+
245
+ def not_like(self, pattern: str) -> ColumnExpression:
246
+ """SQL NOT LIKE."""
247
+ return ~self.like(pattern)
248
+
249
+ def not_ilike(self, pattern: str) -> ColumnExpression:
250
+ """Case-insensitive NOT LIKE."""
251
+ return ~self.ilike(pattern)
252
+
253
+ def between(self, start: Any, end: Any) -> ColumnExpression:
254
+ """SQL BETWEEN clause."""
255
+ return ColumnExpression(exp.Between(this=self._expr, low=exp.convert(start), high=exp.convert(end)))
256
+
257
+ def is_null(self) -> ColumnExpression:
258
+ """SQL IS NULL."""
259
+ return ColumnExpression(exp.Is(this=self._expr, expression=exp.Null()))
260
+
261
+ def is_not_null(self) -> ColumnExpression:
262
+ """SQL IS NOT NULL."""
263
+ return ColumnExpression(exp.Not(this=exp.Is(this=self._expr, expression=exp.Null())))
264
+
265
+ def any_(self, values: Iterable[Any]) -> ColumnExpression:
266
+ """SQL = ANY(...) clause."""
267
+ converted_values = [exp.convert(v) for v in values]
268
+ return ColumnExpression(exp.EQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
269
+
270
+ def not_any_(self, values: Iterable[Any]) -> ColumnExpression:
271
+ """SQL <> ANY(...) clause."""
272
+ converted_values = [exp.convert(v) for v in values]
273
+ return ColumnExpression(exp.NEQ(this=self._expr, expression=exp.Any(expressions=converted_values)))
274
+
275
+ def alias(self, alias_name: str) -> exp.Expression:
276
+ """Create an aliased function expression."""
277
+ return exp.Alias(this=self._expr, alias=alias_name)
278
+
279
+ # Add other operators as needed...
280
+
281
+ def __hash__(self) -> int:
282
+ """Hash based on the SQL expression."""
283
+ return hash(self._expr.sql() if has_sql_method(self._expr) else str(self._expr))