sqlspec 0.15.0__py3-none-any.whl → 0.16.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.

sqlspec/_sql.py CHANGED
@@ -4,17 +4,65 @@ Provides both statement builders (select, insert, update, etc.) and column expre
4
4
  """
5
5
 
6
6
  import logging
7
- from typing import Any, Optional, Union
7
+ from typing import TYPE_CHECKING, Any, Optional, Union
8
8
 
9
9
  import sqlglot
10
10
  from sqlglot import exp
11
11
  from sqlglot.dialects.dialect import DialectType
12
12
  from sqlglot.errors import ParseError as SQLGlotParseError
13
13
 
14
- from sqlspec.builder import Column, Delete, Insert, Merge, Select, Truncate, Update
14
+ from sqlspec.builder import (
15
+ AlterTable,
16
+ Column,
17
+ CommentOn,
18
+ CreateIndex,
19
+ CreateMaterializedView,
20
+ CreateSchema,
21
+ CreateTable,
22
+ CreateTableAsSelect,
23
+ CreateView,
24
+ Delete,
25
+ DropIndex,
26
+ DropSchema,
27
+ DropTable,
28
+ DropView,
29
+ Insert,
30
+ Merge,
31
+ RenameTable,
32
+ Select,
33
+ Truncate,
34
+ Update,
35
+ )
15
36
  from sqlspec.exceptions import SQLBuilderError
16
37
 
17
- __all__ = ("Case", "Column", "Delete", "Insert", "Merge", "SQLFactory", "Select", "Truncate", "Update", "sql")
38
+ if TYPE_CHECKING:
39
+ from sqlspec.core.statement import SQL
40
+
41
+ __all__ = (
42
+ "AlterTable",
43
+ "Case",
44
+ "Column",
45
+ "CommentOn",
46
+ "CreateIndex",
47
+ "CreateMaterializedView",
48
+ "CreateSchema",
49
+ "CreateTable",
50
+ "CreateTableAsSelect",
51
+ "CreateView",
52
+ "Delete",
53
+ "DropIndex",
54
+ "DropSchema",
55
+ "DropTable",
56
+ "DropView",
57
+ "Insert",
58
+ "Merge",
59
+ "RenameTable",
60
+ "SQLFactory",
61
+ "Select",
62
+ "Truncate",
63
+ "Update",
64
+ "sql",
65
+ )
18
66
 
19
67
  logger = logging.getLogger("sqlspec")
20
68
 
@@ -212,6 +260,174 @@ class SQLFactory:
212
260
  return builder.into(table_or_sql)
213
261
  return builder
214
262
 
263
+ # ===================
264
+ # DDL Statement Builders
265
+ # ===================
266
+
267
+ def create_table(self, table_name: str, dialect: DialectType = None) -> "CreateTable":
268
+ """Create a CREATE TABLE builder.
269
+
270
+ Args:
271
+ table_name: Name of the table to create
272
+ dialect: Optional SQL dialect
273
+
274
+ Returns:
275
+ CreateTable builder instance
276
+ """
277
+ builder = CreateTable(table_name)
278
+ builder.dialect = dialect or self.dialect
279
+ return builder
280
+
281
+ def create_table_as_select(self, dialect: DialectType = None) -> "CreateTableAsSelect":
282
+ """Create a CREATE TABLE AS SELECT builder.
283
+
284
+ Args:
285
+ dialect: Optional SQL dialect
286
+
287
+ Returns:
288
+ CreateTableAsSelect builder instance
289
+ """
290
+ builder = CreateTableAsSelect()
291
+ builder.dialect = dialect or self.dialect
292
+ return builder
293
+
294
+ def create_view(self, dialect: DialectType = None) -> "CreateView":
295
+ """Create a CREATE VIEW builder.
296
+
297
+ Args:
298
+ dialect: Optional SQL dialect
299
+
300
+ Returns:
301
+ CreateView builder instance
302
+ """
303
+ builder = CreateView()
304
+ builder.dialect = dialect or self.dialect
305
+ return builder
306
+
307
+ def create_materialized_view(self, dialect: DialectType = None) -> "CreateMaterializedView":
308
+ """Create a CREATE MATERIALIZED VIEW builder.
309
+
310
+ Args:
311
+ dialect: Optional SQL dialect
312
+
313
+ Returns:
314
+ CreateMaterializedView builder instance
315
+ """
316
+ builder = CreateMaterializedView()
317
+ builder.dialect = dialect or self.dialect
318
+ return builder
319
+
320
+ def create_index(self, index_name: str, dialect: DialectType = None) -> "CreateIndex":
321
+ """Create a CREATE INDEX builder.
322
+
323
+ Args:
324
+ index_name: Name of the index to create
325
+ dialect: Optional SQL dialect
326
+
327
+ Returns:
328
+ CreateIndex builder instance
329
+ """
330
+ return CreateIndex(index_name, dialect=dialect or self.dialect)
331
+
332
+ def create_schema(self, dialect: DialectType = None) -> "CreateSchema":
333
+ """Create a CREATE SCHEMA builder.
334
+
335
+ Args:
336
+ dialect: Optional SQL dialect
337
+
338
+ Returns:
339
+ CreateSchema builder instance
340
+ """
341
+ builder = CreateSchema()
342
+ builder.dialect = dialect or self.dialect
343
+ return builder
344
+
345
+ def drop_table(self, table_name: str, dialect: DialectType = None) -> "DropTable":
346
+ """Create a DROP TABLE builder.
347
+
348
+ Args:
349
+ table_name: Name of the table to drop
350
+ dialect: Optional SQL dialect
351
+
352
+ Returns:
353
+ DropTable builder instance
354
+ """
355
+ return DropTable(table_name, dialect=dialect or self.dialect)
356
+
357
+ def drop_view(self, dialect: DialectType = None) -> "DropView":
358
+ """Create a DROP VIEW builder.
359
+
360
+ Args:
361
+ dialect: Optional SQL dialect
362
+
363
+ Returns:
364
+ DropView builder instance
365
+ """
366
+ return DropView(dialect=dialect or self.dialect)
367
+
368
+ def drop_index(self, index_name: str, dialect: DialectType = None) -> "DropIndex":
369
+ """Create a DROP INDEX builder.
370
+
371
+ Args:
372
+ index_name: Name of the index to drop
373
+ dialect: Optional SQL dialect
374
+
375
+ Returns:
376
+ DropIndex builder instance
377
+ """
378
+ return DropIndex(index_name, dialect=dialect or self.dialect)
379
+
380
+ def drop_schema(self, dialect: DialectType = None) -> "DropSchema":
381
+ """Create a DROP SCHEMA builder.
382
+
383
+ Args:
384
+ dialect: Optional SQL dialect
385
+
386
+ Returns:
387
+ DropSchema builder instance
388
+ """
389
+ return DropSchema(dialect=dialect or self.dialect)
390
+
391
+ def alter_table(self, table_name: str, dialect: DialectType = None) -> "AlterTable":
392
+ """Create an ALTER TABLE builder.
393
+
394
+ Args:
395
+ table_name: Name of the table to alter
396
+ dialect: Optional SQL dialect
397
+
398
+ Returns:
399
+ AlterTable builder instance
400
+ """
401
+ builder = AlterTable(table_name)
402
+ builder.dialect = dialect or self.dialect
403
+ return builder
404
+
405
+ def rename_table(self, dialect: DialectType = None) -> "RenameTable":
406
+ """Create a RENAME TABLE builder.
407
+
408
+ Args:
409
+ dialect: Optional SQL dialect
410
+
411
+ Returns:
412
+ RenameTable builder instance
413
+ """
414
+ builder = RenameTable()
415
+ builder.dialect = dialect or self.dialect
416
+ return builder
417
+
418
+ def comment_on(self, dialect: DialectType = None) -> "CommentOn":
419
+ """Create a COMMENT ON builder.
420
+
421
+ Args:
422
+ dialect: Optional SQL dialect
423
+
424
+ Returns:
425
+ CommentOn builder instance
426
+ """
427
+ builder = CommentOn()
428
+ builder.dialect = dialect or self.dialect
429
+ return builder
430
+
215
431
  # ===================
216
432
  # SQL Analysis Helpers
217
433
  # ===================
@@ -363,8 +579,8 @@ class SQLFactory:
363
579
  # ===================
364
580
 
365
581
  @staticmethod
366
- def raw(sql_fragment: str) -> exp.Expression:
367
- """Create a raw SQL expression from a string fragment.
582
+ def raw(sql_fragment: str, **parameters: Any) -> "Union[exp.Expression, SQL]":
583
+ """Create a raw SQL expression from a string fragment with optional parameters.
368
584
 
369
585
  This method makes it explicit that you are passing raw SQL that should
370
586
  be parsed and included directly in the query. Useful for complex expressions,
@@ -372,30 +588,30 @@ class SQLFactory:
372
588
 
373
589
  Args:
374
590
  sql_fragment: Raw SQL string to parse into an expression.
591
+ **parameters: Named parameters for parameter binding.
375
592
 
376
593
  Returns:
377
- SQLGlot expression from the parsed SQL fragment.
594
+ SQLGlot expression from the parsed SQL fragment (if no parameters).
595
+ SQL statement object (if parameters provided).
378
596
 
379
597
  Raises:
380
598
  SQLBuilderError: If the SQL fragment cannot be parsed.
381
599
 
382
600
  Example:
383
601
  ```python
384
- # Raw column expression with alias
385
- query = sql.select(
386
- sql.raw("user.id AS u_id"), "name"
387
- ).from_("users")
602
+ # Raw expression without parameters (current behavior)
603
+ expr = sql.raw("COALESCE(name, 'Unknown')")
388
604
 
389
- # Raw function call
390
- query = sql.select(
391
- sql.raw("COALESCE(name, 'Unknown')")
392
- ).from_("users")
605
+ # Raw SQL with named parameters (new functionality)
606
+ stmt = sql.raw(
607
+ "LOWER(name) LIKE LOWER(:pattern)", pattern=f"%{query}%"
608
+ )
393
609
 
394
- # Raw complex expression
395
- query = (
396
- sql.select("*")
397
- .from_("orders")
398
- .where(sql.raw("DATE(created_at) = CURRENT_DATE"))
610
+ # Raw complex expression with parameters
611
+ expr = sql.raw(
612
+ "price BETWEEN :min_price AND :max_price",
613
+ min_price=100,
614
+ max_price=500,
399
615
  )
400
616
 
401
617
  # Raw window function
@@ -407,16 +623,23 @@ class SQLFactory:
407
623
  ).from_("employees")
408
624
  ```
409
625
  """
410
- try:
411
- parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
412
- if parsed is not None:
413
- return parsed
414
- if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
415
- return exp.to_identifier(sql_fragment)
416
- return exp.Literal.string(sql_fragment)
417
- except Exception as e:
418
- msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
419
- raise SQLBuilderError(msg) from e
626
+ if not parameters:
627
+ # Original behavior - return pure expression
628
+ try:
629
+ parsed: Optional[exp.Expression] = exp.maybe_parse(sql_fragment)
630
+ if parsed is not None:
631
+ return parsed
632
+ if sql_fragment.strip().replace("_", "").replace(".", "").isalnum():
633
+ return exp.to_identifier(sql_fragment)
634
+ return exp.Literal.string(sql_fragment)
635
+ except Exception as e:
636
+ msg = f"Failed to parse raw SQL fragment '{sql_fragment}': {e}"
637
+ raise SQLBuilderError(msg) from e
638
+
639
+ # New behavior - return SQL statement with parameters
640
+ from sqlspec.core.statement import SQL
641
+
642
+ return SQL(sql_fragment, parameters)
420
643
 
421
644
  # ===================
422
645
  # Aggregate Functions
@@ -119,7 +119,18 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
119
119
  msg = ERR_MSG_VALUES_COLUMNS_MISMATCH.format(values_len=len(values), columns_len=len(self._columns))
120
120
  raise SQLBuilderError(msg)
121
121
 
122
- param_names = [self._add_parameter(value) for value in values]
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)
123
134
  value_placeholders = tuple(exp.var(name) for name in param_names)
124
135
 
125
136
  current_values_expression = insert_expr.args.get("expression")
@@ -109,7 +109,11 @@ def parse_condition_expression(
109
109
  if value is None:
110
110
  return exp.Is(this=column_expr, expression=exp.null())
111
111
  if builder and has_parameter_builder(builder):
112
- _, param_name = builder.add_parameter(value)
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)
113
117
  return exp.EQ(this=column_expr, expression=exp.Placeholder(this=param_name))
114
118
  if isinstance(value, str):
115
119
  return exp.EQ(this=column_expr, expression=exp.convert(value))
@@ -78,11 +78,21 @@ class InsertValuesMixin:
78
78
  except AttributeError:
79
79
  pass
80
80
  row_exprs = []
81
- for v in values:
81
+ for i, v in enumerate(values):
82
82
  if isinstance(v, exp.Expression):
83
83
  row_exprs.append(v)
84
84
  else:
85
- _, param_name = self.add_parameter(v) # type: ignore[attr-defined]
85
+ # Try to use column name if available, otherwise use position-based name
86
+ try:
87
+ _columns = self._columns # type: ignore[attr-defined]
88
+ if _columns and i < len(_columns):
89
+ column_name = str(_columns[i]).split(".")[-1] if "." in str(_columns[i]) else str(_columns[i])
90
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
91
+ else:
92
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
93
+ except AttributeError:
94
+ param_name = self._generate_unique_parameter_name(f"value_{i + 1}") # type: ignore[attr-defined]
95
+ _, param_name = self.add_parameter(v, name=param_name) # type: ignore[attr-defined]
86
96
  row_exprs.append(exp.var(param_name))
87
97
  values_expr = exp.Values(expressions=[row_exprs])
88
98
  self._expression.set("expression", values_expr)
@@ -172,7 +172,11 @@ class MergeMatchedClauseMixin:
172
172
  """
173
173
  update_expressions: list[exp.EQ] = []
174
174
  for col, val in set_values.items():
175
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
175
+ column_name = col if isinstance(col, str) else str(col)
176
+ if "." in column_name:
177
+ column_name = column_name.split(".")[-1]
178
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
179
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
176
180
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
177
181
 
178
182
  when_args: dict[str, Any] = {"matched": True, "then": exp.Update(expressions=update_expressions)}
@@ -270,8 +274,12 @@ class MergeNotMatchedClauseMixin:
270
274
  raise SQLBuilderError(msg)
271
275
 
272
276
  parameterized_values: list[exp.Expression] = []
273
- for val in values:
274
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
277
+ for i, val in enumerate(values):
278
+ column_name = columns[i] if isinstance(columns[i], str) else str(columns[i])
279
+ if "." in column_name:
280
+ column_name = column_name.split(".")[-1]
281
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
282
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
275
283
  parameterized_values.append(exp.var(param_name))
276
284
 
277
285
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
@@ -336,7 +344,11 @@ class MergeNotMatchedBySourceClauseMixin:
336
344
  """
337
345
  update_expressions: list[exp.EQ] = []
338
346
  for col, val in set_values.items():
339
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
347
+ column_name = col if isinstance(col, str) else str(col)
348
+ if "." in column_name:
349
+ column_name = column_name.split(".")[-1]
350
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
351
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
340
352
  update_expressions.append(exp.EQ(this=exp.column(col), expression=exp.var(param_name)))
341
353
 
342
354
  when_args: dict[str, Any] = {
@@ -563,7 +563,8 @@ class CaseBuilder:
563
563
  CaseBuilder: The current builder instance for method chaining.
564
564
  """
565
565
  cond_expr = exp.condition(condition) if isinstance(condition, str) else condition
566
- param_name = self._parent.add_parameter(value)[1]
566
+ param_name = self._parent._generate_unique_parameter_name("case_when_value")
567
+ param_name = self._parent.add_parameter(value, name=param_name)[1]
567
568
  value_expr = exp.Placeholder(this=param_name)
568
569
 
569
570
  when_clause = exp.When(this=cond_expr, then=value_expr)
@@ -582,7 +583,8 @@ class CaseBuilder:
582
583
  Returns:
583
584
  CaseBuilder: The current builder instance for method chaining.
584
585
  """
585
- param_name = self._parent.add_parameter(value)[1]
586
+ param_name = self._parent._generate_unique_parameter_name("case_else_value")
587
+ param_name = self._parent.add_parameter(value, name=param_name)[1]
586
588
  value_expr = exp.Placeholder(this=param_name)
587
589
  self._case_expr.set("default", value_expr)
588
590
  return self
@@ -81,7 +81,12 @@ class UpdateSetClauseMixin:
81
81
  for p_name, p_value in val.parameters.items():
82
82
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
83
83
  else:
84
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
84
+ column_name = col if isinstance(col, str) else str(col)
85
+ # Extract just the column part if table.column format
86
+ if "." in column_name:
87
+ column_name = column_name.split(".")[-1]
88
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
89
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
85
90
  value_expr = exp.Placeholder(this=param_name)
86
91
  assignments.append(exp.EQ(this=col_expr, expression=value_expr))
87
92
  elif (len(args) == 1 and isinstance(args[0], Mapping)) or kwargs:
@@ -97,7 +102,12 @@ class UpdateSetClauseMixin:
97
102
  for p_name, p_value in val.parameters.items():
98
103
  self.add_parameter(p_value, name=p_name) # type: ignore[attr-defined]
99
104
  else:
100
- param_name = self.add_parameter(val)[1] # type: ignore[attr-defined]
105
+ # Extract column name for parameter naming
106
+ column_name = col if isinstance(col, str) else str(col)
107
+ if "." in column_name:
108
+ column_name = column_name.split(".")[-1]
109
+ param_name = self._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
110
+ param_name = self.add_parameter(val, name=param_name)[1] # type: ignore[attr-defined]
101
111
  value_expr = exp.Placeholder(this=param_name)
102
112
  assignments.append(exp.EQ(this=exp.column(col), expression=value_expr))
103
113
  else:
@@ -10,6 +10,29 @@ from sqlspec.builder._parsing_utils import parse_column_expression, parse_condit
10
10
  from sqlspec.exceptions import SQLBuilderError
11
11
  from sqlspec.utils.type_guards import has_query_builder_parameters, has_sqlglot_expression, is_iterable_parameters
12
12
 
13
+
14
+ def _extract_column_name(column: Union[str, exp.Column]) -> str:
15
+ """Extract column name from column expression for parameter naming.
16
+
17
+ Args:
18
+ column: Column expression (string or SQLGlot Column)
19
+
20
+ Returns:
21
+ Column name as string for use as parameter name
22
+ """
23
+ if isinstance(column, str):
24
+ # Handle simple column names and table.column references
25
+ if "." in column:
26
+ return column.split(".")[-1] # Return just the column part
27
+ return column
28
+ if isinstance(column, exp.Column):
29
+ # Extract the column name from SQLGlot Column expression
30
+ if column.this and hasattr(column.this, "this"):
31
+ return str(column.this.this)
32
+ return str(column.this) if column.this else "column"
33
+ return "column"
34
+
35
+
13
36
  if TYPE_CHECKING:
14
37
  from sqlspec.builder._column import ColumnExpression
15
38
  from sqlspec.protocols import SQLBuilderProtocol
@@ -20,28 +43,42 @@ __all__ = ("HavingClauseMixin", "WhereClauseMixin")
20
43
  class WhereClauseMixin:
21
44
  """Mixin providing WHERE clause methods for SELECT, UPDATE, and DELETE builders."""
22
45
 
23
- def _handle_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
46
+ def _handle_in_operator(
47
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
48
+ ) -> exp.Expression:
24
49
  """Handle IN operator."""
25
50
  builder = cast("SQLBuilderProtocol", self)
26
51
  if is_iterable_parameters(value):
27
52
  placeholders = []
28
- for v in value:
29
- _, param_name = builder.add_parameter(v)
53
+ for i, v in enumerate(value):
54
+ if len(value) == 1:
55
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
56
+ else:
57
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
58
+ _, param_name = builder.add_parameter(v, name=param_name)
30
59
  placeholders.append(exp.Placeholder(this=param_name))
31
60
  return exp.In(this=column_exp, expressions=placeholders)
32
- _, param_name = builder.add_parameter(value)
61
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
62
+ _, param_name = builder.add_parameter(value, name=param_name)
33
63
  return exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)])
34
64
 
35
- def _handle_not_in_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
65
+ def _handle_not_in_operator(
66
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
67
+ ) -> exp.Expression:
36
68
  """Handle NOT IN operator."""
37
69
  builder = cast("SQLBuilderProtocol", self)
38
70
  if is_iterable_parameters(value):
39
71
  placeholders = []
40
- for v in value:
41
- _, param_name = builder.add_parameter(v)
72
+ for i, v in enumerate(value):
73
+ if len(value) == 1:
74
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
75
+ else:
76
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
77
+ _, param_name = builder.add_parameter(v, name=param_name)
42
78
  placeholders.append(exp.Placeholder(this=param_name))
43
79
  return exp.Not(this=exp.In(this=column_exp, expressions=placeholders))
44
- _, param_name = builder.add_parameter(value)
80
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
81
+ _, param_name = builder.add_parameter(value, name=param_name)
45
82
  return exp.Not(this=exp.In(this=column_exp, expressions=[exp.Placeholder(this=param_name)]))
46
83
 
47
84
  def _handle_is_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
@@ -54,26 +91,34 @@ class WhereClauseMixin:
54
91
  value_expr = exp.Null() if value is None else exp.convert(value)
55
92
  return exp.Not(this=exp.Is(this=column_exp, expression=value_expr))
56
93
 
57
- def _handle_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
94
+ def _handle_between_operator(
95
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
96
+ ) -> exp.Expression:
58
97
  """Handle BETWEEN operator."""
59
98
  if is_iterable_parameters(value) and len(value) == 2:
60
99
  builder = cast("SQLBuilderProtocol", self)
61
100
  low, high = value
62
- _, low_param = builder.add_parameter(low)
63
- _, high_param = builder.add_parameter(high)
101
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
102
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
103
+ _, low_param = builder.add_parameter(low, name=low_param)
104
+ _, high_param = builder.add_parameter(high, name=high_param)
64
105
  return exp.Between(
65
106
  this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
66
107
  )
67
108
  msg = f"BETWEEN operator requires a tuple of two values, got {type(value).__name__}"
68
109
  raise SQLBuilderError(msg)
69
110
 
70
- def _handle_not_between_operator(self, column_exp: exp.Expression, value: Any) -> exp.Expression:
111
+ def _handle_not_between_operator(
112
+ self, column_exp: exp.Expression, value: Any, column_name: str = "column"
113
+ ) -> exp.Expression:
71
114
  """Handle NOT BETWEEN operator."""
72
115
  if is_iterable_parameters(value) and len(value) == 2:
73
116
  builder = cast("SQLBuilderProtocol", self)
74
117
  low, high = value
75
- _, low_param = builder.add_parameter(low)
76
- _, high_param = builder.add_parameter(high)
118
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
119
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
120
+ _, low_param = builder.add_parameter(low, name=low_param)
121
+ _, high_param = builder.add_parameter(high, name=high_param)
77
122
  return exp.Not(
78
123
  this=exp.Between(
79
124
  this=column_exp, low=exp.Placeholder(this=low_param), high=exp.Placeholder(this=high_param)
@@ -85,13 +130,15 @@ class WhereClauseMixin:
85
130
  def _process_tuple_condition(self, condition: tuple) -> exp.Expression:
86
131
  """Process tuple-based WHERE conditions."""
87
132
  builder = cast("SQLBuilderProtocol", self)
88
- column_name = str(condition[0])
89
- column_exp = parse_column_expression(column_name)
133
+ column_name_raw = str(condition[0])
134
+ column_exp = parse_column_expression(column_name_raw)
135
+ column_name = _extract_column_name(column_name_raw)
90
136
 
91
137
  if len(condition) == 2:
92
138
  # (column, value) tuple for equality
93
139
  value = condition[1]
94
- _, param_name = builder.add_parameter(value)
140
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
141
+ _, param_name = builder.add_parameter(value, name=param_name)
95
142
  return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
96
143
 
97
144
  if len(condition) == 3:
@@ -100,42 +147,50 @@ class WhereClauseMixin:
100
147
  value = condition[2]
101
148
 
102
149
  if operator == "=":
103
- _, param_name = builder.add_parameter(value)
150
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
151
+ _, param_name = builder.add_parameter(value, name=param_name)
104
152
  return exp.EQ(this=column_exp, expression=exp.Placeholder(this=param_name))
105
153
  if operator in {"!=", "<>"}:
106
- _, param_name = builder.add_parameter(value)
154
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
155
+ _, param_name = builder.add_parameter(value, name=param_name)
107
156
  return exp.NEQ(this=column_exp, expression=exp.Placeholder(this=param_name))
108
157
  if operator == ">":
109
- _, param_name = builder.add_parameter(value)
158
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
159
+ _, param_name = builder.add_parameter(value, name=param_name)
110
160
  return exp.GT(this=column_exp, expression=exp.Placeholder(this=param_name))
111
161
  if operator == ">=":
112
- _, param_name = builder.add_parameter(value)
162
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
163
+ _, param_name = builder.add_parameter(value, name=param_name)
113
164
  return exp.GTE(this=column_exp, expression=exp.Placeholder(this=param_name))
114
165
  if operator == "<":
115
- _, param_name = builder.add_parameter(value)
166
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
167
+ _, param_name = builder.add_parameter(value, name=param_name)
116
168
  return exp.LT(this=column_exp, expression=exp.Placeholder(this=param_name))
117
169
  if operator == "<=":
118
- _, param_name = builder.add_parameter(value)
170
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
171
+ _, param_name = builder.add_parameter(value, name=param_name)
119
172
  return exp.LTE(this=column_exp, expression=exp.Placeholder(this=param_name))
120
173
  if operator == "LIKE":
121
- _, param_name = builder.add_parameter(value)
174
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
175
+ _, param_name = builder.add_parameter(value, name=param_name)
122
176
  return exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name))
123
177
  if operator == "NOT LIKE":
124
- _, param_name = builder.add_parameter(value)
178
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
179
+ _, param_name = builder.add_parameter(value, name=param_name)
125
180
  return exp.Not(this=exp.Like(this=column_exp, expression=exp.Placeholder(this=param_name)))
126
181
 
127
182
  if operator == "IN":
128
- return self._handle_in_operator(column_exp, value)
183
+ return self._handle_in_operator(column_exp, value, column_name)
129
184
  if operator == "NOT IN":
130
- return self._handle_not_in_operator(column_exp, value)
185
+ return self._handle_not_in_operator(column_exp, value, column_name)
131
186
  if operator == "IS":
132
187
  return self._handle_is_operator(column_exp, value)
133
188
  if operator == "IS NOT":
134
189
  return self._handle_is_not_operator(column_exp, value)
135
190
  if operator == "BETWEEN":
136
- return self._handle_between_operator(column_exp, value)
191
+ return self._handle_between_operator(column_exp, value, column_name)
137
192
  if operator == "NOT BETWEEN":
138
- return self._handle_not_between_operator(column_exp, value)
193
+ return self._handle_not_between_operator(column_exp, value, column_name)
139
194
 
140
195
  msg = f"Unsupported operator: {operator}"
141
196
  raise SQLBuilderError(msg)
@@ -218,86 +273,107 @@ class WhereClauseMixin:
218
273
  def where_eq(self, column: Union[str, exp.Column], value: Any) -> Self:
219
274
  """Add WHERE column = value clause."""
220
275
  builder = cast("SQLBuilderProtocol", self)
221
- _, param_name = builder.add_parameter(value)
276
+ column_name = _extract_column_name(column)
277
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
278
+ _, param_name = builder.add_parameter(value, name=param_name)
222
279
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
223
- condition: exp.Expression = col_expr.eq(exp.var(param_name))
280
+ condition: exp.Expression = col_expr.eq(exp.Placeholder(this=param_name))
224
281
  return self.where(condition)
225
282
 
226
283
  def where_neq(self, column: Union[str, exp.Column], value: Any) -> Self:
227
284
  """Add WHERE column != value clause."""
228
285
  builder = cast("SQLBuilderProtocol", self)
229
- _, param_name = builder.add_parameter(value)
286
+ column_name = _extract_column_name(column)
287
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
288
+ _, param_name = builder.add_parameter(value, name=param_name)
230
289
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
231
- condition: exp.Expression = col_expr.neq(exp.var(param_name))
290
+ condition: exp.Expression = col_expr.neq(exp.Placeholder(this=param_name))
232
291
  return self.where(condition)
233
292
 
234
293
  def where_lt(self, column: Union[str, exp.Column], value: Any) -> Self:
235
294
  """Add WHERE column < value clause."""
236
295
  builder = cast("SQLBuilderProtocol", self)
237
- _, param_name = builder.add_parameter(value)
296
+ column_name = _extract_column_name(column)
297
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
298
+ _, param_name = builder.add_parameter(value, name=param_name)
238
299
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
239
- condition: exp.Expression = exp.LT(this=col_expr, expression=exp.var(param_name))
300
+ condition: exp.Expression = exp.LT(this=col_expr, expression=exp.Placeholder(this=param_name))
240
301
  return self.where(condition)
241
302
 
242
303
  def where_lte(self, column: Union[str, exp.Column], value: Any) -> Self:
243
304
  """Add WHERE column <= value clause."""
244
305
  builder = cast("SQLBuilderProtocol", self)
245
- _, param_name = builder.add_parameter(value)
306
+ column_name = _extract_column_name(column)
307
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
308
+ _, param_name = builder.add_parameter(value, name=param_name)
246
309
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
247
- condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.var(param_name))
310
+ condition: exp.Expression = exp.LTE(this=col_expr, expression=exp.Placeholder(this=param_name))
248
311
  return self.where(condition)
249
312
 
250
313
  def where_gt(self, column: Union[str, exp.Column], value: Any) -> Self:
251
314
  """Add WHERE column > value clause."""
252
315
  builder = cast("SQLBuilderProtocol", self)
253
- _, param_name = builder.add_parameter(value)
316
+ column_name = _extract_column_name(column)
317
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
318
+ _, param_name = builder.add_parameter(value, name=param_name)
254
319
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
255
- condition: exp.Expression = exp.GT(this=col_expr, expression=exp.var(param_name))
320
+ condition: exp.Expression = exp.GT(this=col_expr, expression=exp.Placeholder(this=param_name))
256
321
  return self.where(condition)
257
322
 
258
323
  def where_gte(self, column: Union[str, exp.Column], value: Any) -> Self:
259
324
  """Add WHERE column >= value clause."""
260
325
  builder = cast("SQLBuilderProtocol", self)
261
- _, param_name = builder.add_parameter(value)
326
+ column_name = _extract_column_name(column)
327
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
328
+ _, param_name = builder.add_parameter(value, name=param_name)
262
329
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
263
- condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.var(param_name))
330
+ condition: exp.Expression = exp.GTE(this=col_expr, expression=exp.Placeholder(this=param_name))
264
331
  return self.where(condition)
265
332
 
266
333
  def where_between(self, column: Union[str, exp.Column], low: Any, high: Any) -> Self:
267
334
  """Add WHERE column BETWEEN low AND high clause."""
268
335
  builder = cast("SQLBuilderProtocol", self)
269
- _, low_param = builder.add_parameter(low)
270
- _, high_param = builder.add_parameter(high)
336
+ column_name = _extract_column_name(column)
337
+ low_param = builder._generate_unique_parameter_name(f"{column_name}_low") # type: ignore[attr-defined]
338
+ high_param = builder._generate_unique_parameter_name(f"{column_name}_high") # type: ignore[attr-defined]
339
+ _, low_param = builder.add_parameter(low, name=low_param)
340
+ _, high_param = builder.add_parameter(high, name=high_param)
271
341
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
272
- condition: exp.Expression = col_expr.between(exp.var(low_param), exp.var(high_param))
342
+ condition: exp.Expression = col_expr.between(exp.Placeholder(this=low_param), exp.Placeholder(this=high_param))
273
343
  return self.where(condition)
274
344
 
275
345
  def where_like(self, column: Union[str, exp.Column], pattern: str, escape: Optional[str] = None) -> Self:
276
346
  """Add WHERE column LIKE pattern clause."""
277
347
  builder = cast("SQLBuilderProtocol", self)
278
- _, param_name = builder.add_parameter(pattern)
348
+ column_name = _extract_column_name(column)
349
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
350
+ _, param_name = builder.add_parameter(pattern, name=param_name)
279
351
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
280
352
  if escape is not None:
281
- cond = exp.Like(this=col_expr, expression=exp.var(param_name), escape=exp.convert(str(escape)))
353
+ cond = exp.Like(this=col_expr, expression=exp.Placeholder(this=param_name), escape=exp.convert(str(escape)))
282
354
  else:
283
- cond = col_expr.like(exp.var(param_name))
355
+ cond = col_expr.like(exp.Placeholder(this=param_name))
284
356
  condition: exp.Expression = cond
285
357
  return self.where(condition)
286
358
 
287
359
  def where_not_like(self, column: Union[str, exp.Column], pattern: str) -> Self:
288
360
  """Add WHERE column NOT LIKE pattern clause."""
289
361
  builder = cast("SQLBuilderProtocol", self)
290
- _, param_name = builder.add_parameter(pattern)
362
+ column_name = _extract_column_name(column)
363
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
364
+ _, param_name = builder.add_parameter(pattern, name=param_name)
291
365
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
292
- condition: exp.Expression = col_expr.like(exp.var(param_name)).not_()
366
+ condition: exp.Expression = col_expr.like(exp.Placeholder(this=param_name)).not_()
293
367
  return self.where(condition)
294
368
 
295
369
  def where_ilike(self, column: Union[str, exp.Column], pattern: str) -> Self:
296
370
  """Add WHERE column ILIKE pattern clause."""
297
371
  builder = cast("SQLBuilderProtocol", self)
298
- _, param_name = builder.add_parameter(pattern)
372
+ column_name = _extract_column_name(column)
373
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
374
+ _, param_name = builder.add_parameter(pattern, name=param_name)
299
375
  col_expr = parse_column_expression(column) if not isinstance(column, exp.Column) else column
300
- condition: exp.Expression = col_expr.ilike(exp.var(param_name))
376
+ condition: exp.Expression = col_expr.ilike(exp.Placeholder(this=param_name))
301
377
  return self.where(condition)
302
378
 
303
379
  def where_is_null(self, column: Union[str, exp.Column]) -> Self:
@@ -322,6 +398,10 @@ class WhereClauseMixin:
322
398
  subquery = values.build() # pyright: ignore
323
399
  sql_str = subquery.sql
324
400
  subquery_exp = exp.paren(exp.maybe_parse(sql_str, dialect=builder.dialect_name)) # pyright: ignore
401
+ # Merge subquery parameters into parent builder
402
+ if hasattr(subquery, "parameters"):
403
+ for param_name, param_value in subquery.parameters.items(): # pyright: ignore[reportAttributeAccessIssue]
404
+ builder.add_parameter(param_value, name=param_name)
325
405
  else:
326
406
  subquery_exp = values # type: ignore[assignment]
327
407
  condition = col_expr.isin(subquery_exp)
@@ -329,10 +409,15 @@ class WhereClauseMixin:
329
409
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
330
410
  msg = "Unsupported type for 'values' in WHERE IN"
331
411
  raise SQLBuilderError(msg)
412
+ column_name = _extract_column_name(column)
332
413
  parameters = []
333
- for v in values:
334
- _, param_name = builder.add_parameter(v)
335
- parameters.append(exp.var(param_name))
414
+ for i, v in enumerate(values):
415
+ if len(values) == 1:
416
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
417
+ else:
418
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
419
+ _, param_name = builder.add_parameter(v, name=param_name)
420
+ parameters.append(exp.Placeholder(this=param_name))
336
421
  condition = col_expr.isin(*parameters)
337
422
  return self.where(condition)
338
423
 
@@ -353,10 +438,15 @@ class WhereClauseMixin:
353
438
  if not is_iterable_parameters(values) or isinstance(values, (str, bytes)):
354
439
  msg = "Values for where_not_in must be a non-string iterable or subquery."
355
440
  raise SQLBuilderError(msg)
441
+ column_name = _extract_column_name(column)
356
442
  parameters = []
357
- for v in values:
358
- _, param_name = builder.add_parameter(v)
359
- parameters.append(exp.var(param_name))
443
+ for i, v in enumerate(values):
444
+ if len(values) == 1:
445
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
446
+ else:
447
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_{i + 1}") # type: ignore[attr-defined]
448
+ _, param_name = builder.add_parameter(v, name=param_name)
449
+ parameters.append(exp.Placeholder(this=param_name))
360
450
  condition = exp.Not(this=col_expr.isin(*parameters))
361
451
  return self.where(condition)
362
452
 
@@ -438,10 +528,15 @@ class WhereClauseMixin:
438
528
  if not is_iterable_parameters(values) or isinstance(values, bytes):
439
529
  msg = "Unsupported type for 'values' in WHERE ANY"
440
530
  raise SQLBuilderError(msg)
531
+ column_name = _extract_column_name(column)
441
532
  parameters = []
442
- for v in values:
443
- _, param_name = builder.add_parameter(v)
444
- parameters.append(exp.var(param_name))
533
+ for i, v in enumerate(values):
534
+ if len(values) == 1:
535
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
536
+ else:
537
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_any_{i + 1}") # type: ignore[attr-defined]
538
+ _, param_name = builder.add_parameter(v, name=param_name)
539
+ parameters.append(exp.Placeholder(this=param_name))
445
540
  tuple_expr = exp.Tuple(expressions=parameters)
446
541
  condition = exp.EQ(this=col_expr, expression=exp.Any(this=tuple_expr))
447
542
  return self.where(condition)
@@ -473,10 +568,15 @@ class WhereClauseMixin:
473
568
  if not is_iterable_parameters(values) or isinstance(values, bytes):
474
569
  msg = "Unsupported type for 'values' in WHERE NOT ANY"
475
570
  raise SQLBuilderError(msg)
571
+ column_name = _extract_column_name(column)
476
572
  parameters = []
477
- for v in values:
478
- _, param_name = builder.add_parameter(v)
479
- parameters.append(exp.var(param_name))
573
+ for i, v in enumerate(values):
574
+ if len(values) == 1:
575
+ param_name = builder._generate_unique_parameter_name(column_name) # type: ignore[attr-defined]
576
+ else:
577
+ param_name = builder._generate_unique_parameter_name(f"{column_name}_not_any_{i + 1}") # type: ignore[attr-defined]
578
+ _, param_name = builder.add_parameter(v, name=param_name)
579
+ parameters.append(exp.Placeholder(this=param_name))
480
580
  tuple_expr = exp.Tuple(expressions=parameters)
481
581
  condition = exp.NEQ(this=col_expr, expression=exp.Any(this=tuple_expr))
482
582
  return self.where(condition)
sqlspec/core/filters.py CHANGED
@@ -558,6 +558,7 @@ class LimitOffsetFilter(PaginationFilter):
558
558
  return [], {self._limit_param_name: self.limit, self._offset_param_name: self.offset}
559
559
 
560
560
  def append_to_statement(self, statement: "SQL") -> "SQL":
561
+ import sqlglot
561
562
  from sqlglot import exp
562
563
 
563
564
  # Resolve parameter name conflicts
@@ -567,17 +568,18 @@ class LimitOffsetFilter(PaginationFilter):
567
568
  limit_placeholder = exp.Placeholder(this=limit_param_name)
568
569
  offset_placeholder = exp.Placeholder(this=offset_param_name)
569
570
 
570
- if statement._statement is None:
571
- new_statement = exp.Select().limit(limit_placeholder)
572
- else:
573
- new_statement = (
574
- statement._statement.limit(limit_placeholder)
575
- if isinstance(statement._statement, exp.Select)
576
- else exp.Select().from_(statement._statement).limit(limit_placeholder)
577
- )
571
+ # Parse the current SQL to get the statement structure
572
+ try:
573
+ current_statement = sqlglot.parse_one(statement._raw_sql, dialect=getattr(statement, "_dialect", None))
574
+ except Exception:
575
+ # Fallback to wrapping in subquery if parsing fails
576
+ current_statement = exp.Select().from_(f"({statement._raw_sql})")
578
577
 
579
- if isinstance(new_statement, exp.Select):
580
- new_statement = new_statement.offset(offset_placeholder)
578
+ if isinstance(current_statement, exp.Select):
579
+ new_statement = current_statement.limit(limit_placeholder).offset(offset_placeholder)
580
+ else:
581
+ # Wrap non-SELECT statements in a subquery
582
+ new_statement = exp.Select().from_(current_statement).limit(limit_placeholder).offset(offset_placeholder)
581
583
 
582
584
  result = statement.copy(statement=new_statement)
583
585
 
sqlspec/core/statement.py CHANGED
@@ -368,6 +368,10 @@ class SQL:
368
368
  new_sql = SQL(
369
369
  self._raw_sql, *self._original_parameters, statement_config=self._statement_config, is_many=self._is_many
370
370
  )
371
+ # Preserve accumulated parameters when marking as script
372
+ new_sql._named_parameters.update(self._named_parameters)
373
+ new_sql._positional_parameters = self._positional_parameters.copy()
374
+ new_sql._filters = self._filters.copy()
371
375
  new_sql._is_script = True
372
376
  return new_sql
373
377
 
@@ -375,13 +379,19 @@ class SQL:
375
379
  self, statement: "Optional[Union[str, exp.Expression]]" = None, parameters: Optional[Any] = None, **kwargs: Any
376
380
  ) -> "SQL":
377
381
  """Create copy with modifications."""
378
- return SQL(
382
+ new_sql = SQL(
379
383
  statement or self._raw_sql,
380
384
  *(parameters if parameters is not None else self._original_parameters),
381
385
  statement_config=self._statement_config,
382
386
  is_many=self._is_many,
383
387
  **kwargs,
384
388
  )
389
+ # Only preserve accumulated parameters when no explicit parameters are provided
390
+ if parameters is None:
391
+ new_sql._named_parameters.update(self._named_parameters)
392
+ new_sql._positional_parameters = self._positional_parameters.copy()
393
+ new_sql._filters = self._filters.copy()
394
+ return new_sql
385
395
 
386
396
  def add_named_parameter(self, name: str, value: Any) -> "SQL":
387
397
  """Add a named parameter and return a new SQL instance.
@@ -437,9 +447,14 @@ class SQL:
437
447
 
438
448
  new_sql_text = new_expr.sql(dialect=self._dialect)
439
449
 
440
- return SQL(
450
+ new_sql = SQL(
441
451
  new_sql_text, *self._original_parameters, statement_config=self._statement_config, is_many=self._is_many
442
452
  )
453
+ # Preserve accumulated named parameters when creating WHERE clause
454
+ new_sql._named_parameters.update(self._named_parameters)
455
+ new_sql._positional_parameters = self._positional_parameters.copy()
456
+ new_sql._filters = self._filters.copy()
457
+ return new_sql
443
458
 
444
459
  def __hash__(self) -> int:
445
460
  """Hash value."""
@@ -5,7 +5,7 @@ from collections.abc import Sequence
5
5
  from enum import Enum
6
6
  from functools import partial
7
7
  from pathlib import Path, PurePath
8
- from typing import TYPE_CHECKING, Any, Callable, Optional, Union, overload
8
+ from typing import Any, Callable, Optional, overload
9
9
  from uuid import UUID
10
10
 
11
11
  from mypy_extensions import trait
@@ -13,8 +13,6 @@ from mypy_extensions import trait
13
13
  from sqlspec.exceptions import SQLSpecError, wrap_exceptions
14
14
  from sqlspec.typing import (
15
15
  CATTRS_INSTALLED,
16
- DataclassProtocol,
17
- DictLike,
18
16
  ModelDTOT,
19
17
  ModelT,
20
18
  attrs_asdict,
@@ -25,9 +23,6 @@ from sqlspec.typing import (
25
23
  )
26
24
  from sqlspec.utils.type_guards import is_attrs_schema, is_dataclass, is_msgspec_struct, is_pydantic_model
27
25
 
28
- if TYPE_CHECKING:
29
- from sqlspec._typing import AttrsInstanceStub, BaseModelStub, StructStub
30
-
31
26
  __all__ = ("_DEFAULT_TYPE_DECODERS", "_default_msgspec_deserializer")
32
27
 
33
28
 
@@ -74,36 +69,37 @@ class ToSchemaMixin:
74
69
  # Schema conversion overloads - handle common cases first
75
70
  @overload
76
71
  @staticmethod
72
+ def to_schema(data: "list[dict[str, Any]]") -> "list[dict[str, Any]]": ...
73
+ @overload
74
+ @staticmethod
77
75
  def to_schema(data: "list[dict[str, Any]]", *, schema_type: "type[ModelDTOT]") -> "list[ModelDTOT]": ...
78
76
  @overload
79
77
  @staticmethod
80
78
  def to_schema(data: "list[dict[str, Any]]", *, schema_type: None = None) -> "list[dict[str, Any]]": ...
81
79
  @overload
82
80
  @staticmethod
81
+ def to_schema(data: "dict[str, Any]") -> "dict[str, Any]": ...
82
+ @overload
83
+ @staticmethod
83
84
  def to_schema(data: "dict[str, Any]", *, schema_type: "type[ModelDTOT]") -> "ModelDTOT": ...
84
85
  @overload
85
86
  @staticmethod
86
87
  def to_schema(data: "dict[str, Any]", *, schema_type: None = None) -> "dict[str, Any]": ...
87
88
  @overload
88
89
  @staticmethod
90
+ def to_schema(data: "list[ModelT]") -> "list[ModelT]": ...
91
+ @overload
92
+ @staticmethod
89
93
  def to_schema(data: "list[ModelT]", *, schema_type: "type[ModelDTOT]") -> "list[ModelDTOT]": ...
90
94
  @overload
91
95
  @staticmethod
92
96
  def to_schema(data: "list[ModelT]", *, schema_type: None = None) -> "list[ModelT]": ...
93
97
  @overload
94
98
  @staticmethod
95
- def to_schema(
96
- data: "Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]",
97
- *,
98
- schema_type: "type[ModelDTOT]",
99
- ) -> "ModelDTOT": ...
99
+ def to_schema(data: "ModelT") -> "ModelT": ...
100
100
  @overload
101
101
  @staticmethod
102
- def to_schema(
103
- data: "Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]",
104
- *,
105
- schema_type: None = None,
106
- ) -> "Union[DictLike, StructStub, BaseModelStub, DataclassProtocol, AttrsInstanceStub]": ...
102
+ def to_schema(data: Any, *, schema_type: None = None) -> Any: ...
107
103
 
108
104
  @staticmethod
109
105
  def to_schema(data: Any, *, schema_type: "Optional[type[ModelDTOT]]" = None) -> Any:
@@ -39,7 +39,7 @@ def get_database_migration_plugin(app: "Litestar") -> "SQLSpec":
39
39
  raise ImproperConfigurationError(msg)
40
40
 
41
41
 
42
- @click.group(cls=LitestarGroup, name="database")
42
+ @click.group(cls=LitestarGroup, name="db")
43
43
  def database_group(ctx: "click.Context") -> None:
44
44
  """Manage SQLSpec database components."""
45
45
  ctx.obj = {"app": ctx.obj, "configs": get_database_migration_plugin(ctx.obj.app).config}
@@ -1,7 +1,7 @@
1
1
  from typing import TYPE_CHECKING, Any, Union
2
2
 
3
3
  from litestar.di import Provide
4
- from litestar.plugins import InitPluginProtocol
4
+ from litestar.plugins import CLIPlugin, InitPluginProtocol
5
5
 
6
6
  from sqlspec.base import SQLSpec as SQLSpecBase
7
7
  from sqlspec.config import AsyncConfigT, DatabaseConfigProtocol, DriverT, SyncConfigT
@@ -17,7 +17,7 @@ if TYPE_CHECKING:
17
17
  logger = get_logger("extensions.litestar")
18
18
 
19
19
 
20
- class SQLSpec(InitPluginProtocol, SQLSpecBase):
20
+ class SQLSpec(InitPluginProtocol, CLIPlugin, SQLSpecBase):
21
21
  """Litestar plugin for SQLSpec database integration."""
22
22
 
23
23
  __slots__ = ("_config", "_plugin_configs")
@@ -14,7 +14,7 @@ if TYPE_CHECKING:
14
14
  try:
15
15
  import uvloop # pyright: ignore[reportMissingImports]
16
16
  except ImportError:
17
- uvloop = None
17
+ uvloop = None # type: ignore[assignment,unused-ignore]
18
18
 
19
19
 
20
20
  ReturnT = TypeVar("ReturnT")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlspec
3
- Version: 0.15.0
3
+ Version: 0.16.0
4
4
  Summary: SQL Experiments in Python
5
5
  Project-URL: Discord, https://discord.gg/litestar
6
6
  Project-URL: Issue, https://github.com/litestar-org/sqlspec/issues/
@@ -2,7 +2,7 @@ sqlspec/__init__.py,sha256=8_TR9bXd7bkA4qBGCzTHNawH7KXaJ4YlnCJzJwvgud8,2066
2
2
  sqlspec/__main__.py,sha256=lXBKZMOXA1uY735Rnsb-GS7aXy0nt22tYmd2X9FcxrY,253
3
3
  sqlspec/__metadata__.py,sha256=IUw6MCTy1oeUJ1jAVYbuJLkOWbiAWorZ5W-E-SAD9N4,395
4
4
  sqlspec/_serialization.py,sha256=6U5-smk2h2yl0i6am2prtOLJTdu4NJQdcLlSfSUMaUQ,2590
5
- sqlspec/_sql.py,sha256=GggpMLOqVQxuirCcpSj-wBQX2N89jOqNibP7r88-dMY,39567
5
+ sqlspec/_sql.py,sha256=HbJYrs9NCEUQhFermrcS5fDhm3aRxbabLLESHf2aMQg,45544
6
6
  sqlspec/_typing.py,sha256=jv-7QHGLrJLfnP86bR-Xcmj3PDoddNZEKDz_vYRBiAU,22684
7
7
  sqlspec/base.py,sha256=lVLzFD-nzEU6QnwnU0kRWh3XWjbvXWX0XnnUViYBoQk,21767
8
8
  sqlspec/cli.py,sha256=3ZxPwl4neNWyrAkM9J9ccC_gaFigDJbhuZfx15JVE7E,9903
@@ -59,47 +59,47 @@ sqlspec/builder/_column.py,sha256=sGDoRhgeqNv69M2jIcNDWxTsMV3TKRNtktpDwl_ssSI,13
59
59
  sqlspec/builder/_ddl.py,sha256=6TvCMiJAuPBtwXbAupGKnrJ9Wb9-VK5P6cfEFbE9CT8,50284
60
60
  sqlspec/builder/_ddl_utils.py,sha256=qlKT9TqlaymwZLiZjHXIVvFofiWekCLNu-AvPFzUiac,4115
61
61
  sqlspec/builder/_delete.py,sha256=YCAV7GUutY-j1Z6tQ9Q0CE4K4Z5LbZXldMOpS2uxAi4,2301
62
- sqlspec/builder/_insert.py,sha256=nsAaJpAWU0g8LnAJynScn5Iqiw8f-bxvKlT-g5yToxw,8489
62
+ sqlspec/builder/_insert.py,sha256=ozqnpsQUcqzjvGvcgaUKzvrH9GKisY2DGYR7EwXdi_E,9089
63
63
  sqlspec/builder/_merge.py,sha256=pabh0G7DsMZoOG8cGep0qLLdgnyoxQFRUUglhE5yScE,1570
64
- sqlspec/builder/_parsing_utils.py,sha256=0Bu2G5VAWOym-tIpdFau3_sa71CMmM-BwZzgDoD89xE,5384
64
+ sqlspec/builder/_parsing_utils.py,sha256=3XRrL2T2eG_aHfF1kQID9dZpxIgVAkB4u8Mzl1Nm-Cs,5617
65
65
  sqlspec/builder/_select.py,sha256=oxi-5ezjsAt7n6OQm1FNCSO_wFo8djWBPL9f-kiUo9g,5992
66
66
  sqlspec/builder/_update.py,sha256=l9tHwC96VSmk8zGMhzB_S4YuD2_6q4cBXMboqoqvLJ0,5969
67
67
  sqlspec/builder/mixins/__init__.py,sha256=YXhAzKmQbQtne5j26SKWY8PUxwosl0RhlhLoahAdkj0,1885
68
68
  sqlspec/builder/mixins/_cte_and_set_ops.py,sha256=BPPkDTFo2Feja7Vers0BrC_JIYcpfOYSTZ9GIV_UOZ4,8317
69
69
  sqlspec/builder/mixins/_delete_operations.py,sha256=0f8ZZMFx5b53EUx1oF264owLb6aWP45naeB6g-BZQys,1068
70
- sqlspec/builder/mixins/_insert_operations.py,sha256=0Pz0QSa4bgGIiKT2Y8zPs6C2uxEQPyQqXdhKADttSJ0,5528
70
+ sqlspec/builder/mixins/_insert_operations.py,sha256=O2Au5gnoPXTsLtPrlqtHwonIKgY4MV6nkIdzXr3gySk,6340
71
71
  sqlspec/builder/mixins/_join_operations.py,sha256=5qloj7JmRWBi7pFBpAY1acwg_O_1CpcHeYMbzkBKfBE,5507
72
- sqlspec/builder/mixins/_merge_operations.py,sha256=JpP33NevlhfCHnKMpLQOU8THN2ImFKMUwdWiI6UydPI,16745
72
+ sqlspec/builder/mixins/_merge_operations.py,sha256=AnbZ53hXNsfCXdfOvZvSSLW-359R3XwaQD8y_-EBpZc,17642
73
73
  sqlspec/builder/mixins/_order_limit_operations.py,sha256=NuNUb_DJKTdnbjasjOh27zLtbr1JZ4ygS0NH76G4foI,4514
74
74
  sqlspec/builder/mixins/_pivot_operations.py,sha256=nkV9kG15rSoX0IWRAmaZjxyY8J7_5sScgxrRtYjddqE,5886
75
- sqlspec/builder/mixins/_select_operations.py,sha256=-zo75-lp9T3j7D7zRpsa1D3oCd0Jt2xZ_OslhZeumt0,24692
76
- sqlspec/builder/mixins/_update_operations.py,sha256=N6Lm-BrMhEPfFQz9V3-4ZgvSEsGfWtuQwlWbzdgLVWc,6904
77
- sqlspec/builder/mixins/_where_clause.py,sha256=lb6iGqbKPX7rInY1YwYpVMQxUlRftRZthZdwDbnYs20,25729
75
+ sqlspec/builder/mixins/_select_operations.py,sha256=VvmMUmz2Fyhsv-J5O-3iLxMi59tupmgekPLCYxACJSg,24896
76
+ sqlspec/builder/mixins/_update_operations.py,sha256=j4jN1gJnFu-9PG14xgoqXOW1-9GMb__1yLs_GXExWMA,7649
77
+ sqlspec/builder/mixins/_where_clause.py,sha256=tQ3n48tOwlI67oqtY2BV0Tuv-TDNP8N9ceyxKL_92yQ,33289
78
78
  sqlspec/core/__init__.py,sha256=rU_xGsXhqIOnBbyB2InhJknYePm5NQ2DSWdBigror4g,1775
79
79
  sqlspec/core/cache.py,sha256=CQ_xTBQVJaBxB5-ipiw4Oj6JObrcMFtXYRLMSvbXGnE,27130
80
80
  sqlspec/core/compiler.py,sha256=we_ifDr7JOAu3EycmWDxynOYq8ufjwcZl9MDXFTf5cQ,13959
81
- sqlspec/core/filters.py,sha256=J2tpDo8Ton98Gwc9Tx0Ki3X0JgO6xrnbE0_stfBeP3E,31126
81
+ sqlspec/core/filters.py,sha256=X0wRd0vNOFgeOK98ReeTyKt408GCnnmE9p45Bvur3kw,31351
82
82
  sqlspec/core/hashing.py,sha256=4KyAFWtFDMYreoBGGPQppEuMWO6_NrRYlw9Lm-qeJqo,10429
83
83
  sqlspec/core/parameters.py,sha256=X5e9SKGS1gkn7Sv6qAkzZtllIQDmhuUxAnLFlxBkAns,52833
84
84
  sqlspec/core/result.py,sha256=vdPXLApkS0F2Jz-Bq7-A-bKf7g4JIs5_57nAI4O4tC0,21100
85
85
  sqlspec/core/splitter.py,sha256=cb2P1B0q5vvALHi3SEJ7VdbRHH2GWsftrmJi9PYMbeE,28089
86
- sqlspec/core/statement.py,sha256=S_fkqYkG9v2KCsznCBAJIDkR2ASKwub5Y3dNlRYkLec,24231
86
+ sqlspec/core/statement.py,sha256=XdS8YUMEVR1qVjE48a0dIh8554Tm8lTLsa4z7sgM2vE,25117
87
87
  sqlspec/driver/__init__.py,sha256=QVpDRQGd1GreIP199en6qDbq-cZJcEF5go68DINagUk,569
88
88
  sqlspec/driver/_async.py,sha256=0P3VlBYXfythkh9c1wCE3oZMwQa0NtbW_6SljPxpcAw,18858
89
89
  sqlspec/driver/_common.py,sha256=go-SZRYlx_qEh8A9BGFl_BVByEdnLvtSXi6bqA6wOmU,25553
90
90
  sqlspec/driver/_sync.py,sha256=UZKCTrKh48lzRONk-eX5q6qKBLZOawODaPN_28ObACE,18586
91
91
  sqlspec/driver/mixins/__init__.py,sha256=gN4pQyJXxNy0xi91dcMJGA7DQ7TbjGjQI24SSpZc6Go,248
92
- sqlspec/driver/mixins/_result_tools.py,sha256=wI8bGfDrGrG8nexPWiJdvgy1yi1_z643yKPsgMzBqiY,6915
92
+ sqlspec/driver/mixins/_result_tools.py,sha256=s1NWsE_Tiq7R_f6rSqjbPm6SFVYpRBdSdCb0ejlX66c,6733
93
93
  sqlspec/driver/mixins/_sql_translator.py,sha256=zpaJsS0_-zVvm3u2yCQ8VkILrZ7j28925SyZe1ChvlM,1491
94
94
  sqlspec/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
95
95
  sqlspec/extensions/aiosql/__init__.py,sha256=-9cefc9pYPf9vCgALoB-y1DtmcgRjKe2azfl6RIarAA,414
96
96
  sqlspec/extensions/aiosql/adapter.py,sha256=CXkNZaZq86ZhfYFGx4IFbkHmbIFQKMd9CS6Q2jkMCok,16009
97
97
  sqlspec/extensions/litestar/__init__.py,sha256=tOmQ7RHSWOot7p30gk0efxxuP0OCq1opyyZqNmQY7FE,320
98
98
  sqlspec/extensions/litestar/_utils.py,sha256=o-FuUj1_WkDrLxQxiP6hXDak66XfyRP3QLyEVKrIRjI,1954
99
- sqlspec/extensions/litestar/cli.py,sha256=NYjPIWe0jESBgS0AG-3bdpcC5YM4RmZeeeTeFQK5Yqo,1362
99
+ sqlspec/extensions/litestar/cli.py,sha256=X4DlAx3Ry-ccOjAQSxe8SMtyJKCFJVLTbENPU_efKuU,1356
100
100
  sqlspec/extensions/litestar/config.py,sha256=3UI_vhtbupCLsf1nhUgUpRlCoUS5c0GsAjWvegT0c3c,4462
101
101
  sqlspec/extensions/litestar/handlers.py,sha256=3LreU8rZvuHaJnKlN09ttu4wSorWJedsuKgeLT-cOEc,9993
102
- sqlspec/extensions/litestar/plugin.py,sha256=lojdLit4Uop6fpJWGXgpWrbGUBgCWR_khOK4CdRYpKM,5615
102
+ sqlspec/extensions/litestar/plugin.py,sha256=I0aRnL4oZPUYk7pYhZSL3yikl7ViM0kr33kVmH4W-MU,5637
103
103
  sqlspec/extensions/litestar/providers.py,sha256=5LRb5JvRV_XZdNOKkdaIy3j5x-dFCcAi1ea1pgwuapI,18882
104
104
  sqlspec/migrations/__init__.py,sha256=n9y2yLQb02zMz36bPXnrInsSIMLunwHvO7e8UvCYBJc,1061
105
105
  sqlspec/migrations/base.py,sha256=D5-k0m4SnmVU0QRRPcMCvMAyEoAaceYYmD8JprSozDA,13251
@@ -123,12 +123,12 @@ sqlspec/utils/logging.py,sha256=ZEaj7J8oALrs_-p535vSUFo2tfrHkOEcL5pK2JGGFgM,3708
123
123
  sqlspec/utils/module_loader.py,sha256=m5PSN9NwOLd0ZJBuqMVYVi-vaIQMBCVd25vnM3-rv3k,2823
124
124
  sqlspec/utils/serializers.py,sha256=TKsRryRcYMnb8Z8MGkYGClIxcYvC8CW7MsrPQTJqEcY,154
125
125
  sqlspec/utils/singleton.py,sha256=SKnszJi1NPeERgX7IjVIGYAYx4XqR1E_rph3bU6olAU,1047
126
- sqlspec/utils/sync_tools.py,sha256=Iv1Msj1tb24iJhhulL_wcNsyjRComzAkyUNlefVci3w,7348
126
+ sqlspec/utils/sync_tools.py,sha256=WRuk1ZEhb_0CRrumAdnmi-i-dV6qVd3cgJyZw8RY9QQ,7390
127
127
  sqlspec/utils/text.py,sha256=n5K0gvXvyCc8jNteNKsBOymwf_JnQ65f3lu0YaYq4Ys,2898
128
128
  sqlspec/utils/type_guards.py,sha256=AJKSQ323mNfhRbytI2e4N2cy2P6MMG7fp75foVnz328,30339
129
- sqlspec-0.15.0.dist-info/METADATA,sha256=GFcTBVzGgY-1_Wc-fL_9l_lt2JN1i__L4uN9uT6rOhw,16822
130
- sqlspec-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
- sqlspec-0.15.0.dist-info/entry_points.txt,sha256=G-ZqY1Nuuw3Iys7nXw23f6ILenk_Lt47VdK2mhJCWHg,53
132
- sqlspec-0.15.0.dist-info/licenses/LICENSE,sha256=MdujfZ6l5HuLz4mElxlu049itenOR3gnhN1_Nd3nVcM,1078
133
- sqlspec-0.15.0.dist-info/licenses/NOTICE,sha256=Lyir8ozXWov7CyYS4huVaOCNrtgL17P-bNV-5daLntQ,1634
134
- sqlspec-0.15.0.dist-info/RECORD,,
129
+ sqlspec-0.16.0.dist-info/METADATA,sha256=YmaZ4D_0DDG-phbhQMQFcP0lEYVPCpTtd7rJdzEL0BU,16822
130
+ sqlspec-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
+ sqlspec-0.16.0.dist-info/entry_points.txt,sha256=G-ZqY1Nuuw3Iys7nXw23f6ILenk_Lt47VdK2mhJCWHg,53
132
+ sqlspec-0.16.0.dist-info/licenses/LICENSE,sha256=MdujfZ6l5HuLz4mElxlu049itenOR3gnhN1_Nd3nVcM,1078
133
+ sqlspec-0.16.0.dist-info/licenses/NOTICE,sha256=Lyir8ozXWov7CyYS4huVaOCNrtgL17P-bNV-5daLntQ,1634
134
+ sqlspec-0.16.0.dist-info/RECORD,,