sqlspec 0.17.0__py3-none-any.whl → 0.18.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 (80) hide show
  1. sqlspec/__init__.py +1 -1
  2. sqlspec/_sql.py +188 -234
  3. sqlspec/adapters/adbc/config.py +24 -30
  4. sqlspec/adapters/adbc/driver.py +42 -61
  5. sqlspec/adapters/aiosqlite/config.py +5 -10
  6. sqlspec/adapters/aiosqlite/driver.py +9 -25
  7. sqlspec/adapters/aiosqlite/pool.py +43 -35
  8. sqlspec/adapters/asyncmy/config.py +10 -7
  9. sqlspec/adapters/asyncmy/driver.py +18 -39
  10. sqlspec/adapters/asyncpg/config.py +4 -0
  11. sqlspec/adapters/asyncpg/driver.py +32 -79
  12. sqlspec/adapters/bigquery/config.py +12 -65
  13. sqlspec/adapters/bigquery/driver.py +39 -133
  14. sqlspec/adapters/duckdb/config.py +11 -15
  15. sqlspec/adapters/duckdb/driver.py +61 -85
  16. sqlspec/adapters/duckdb/pool.py +2 -5
  17. sqlspec/adapters/oracledb/_types.py +8 -1
  18. sqlspec/adapters/oracledb/config.py +55 -38
  19. sqlspec/adapters/oracledb/driver.py +35 -92
  20. sqlspec/adapters/oracledb/migrations.py +257 -0
  21. sqlspec/adapters/psqlpy/config.py +13 -9
  22. sqlspec/adapters/psqlpy/driver.py +28 -103
  23. sqlspec/adapters/psycopg/config.py +9 -5
  24. sqlspec/adapters/psycopg/driver.py +107 -175
  25. sqlspec/adapters/sqlite/config.py +7 -5
  26. sqlspec/adapters/sqlite/driver.py +37 -73
  27. sqlspec/adapters/sqlite/pool.py +3 -12
  28. sqlspec/base.py +1 -8
  29. sqlspec/builder/__init__.py +1 -1
  30. sqlspec/builder/_base.py +34 -20
  31. sqlspec/builder/_column.py +5 -1
  32. sqlspec/builder/_ddl.py +407 -183
  33. sqlspec/builder/_expression_wrappers.py +46 -0
  34. sqlspec/builder/_insert.py +2 -4
  35. sqlspec/builder/_update.py +5 -5
  36. sqlspec/builder/mixins/_insert_operations.py +26 -6
  37. sqlspec/builder/mixins/_merge_operations.py +1 -1
  38. sqlspec/builder/mixins/_order_limit_operations.py +16 -4
  39. sqlspec/builder/mixins/_select_operations.py +3 -7
  40. sqlspec/builder/mixins/_update_operations.py +4 -4
  41. sqlspec/config.py +32 -13
  42. sqlspec/core/__init__.py +89 -14
  43. sqlspec/core/cache.py +57 -104
  44. sqlspec/core/compiler.py +57 -112
  45. sqlspec/core/filters.py +1 -21
  46. sqlspec/core/hashing.py +13 -47
  47. sqlspec/core/parameters.py +272 -261
  48. sqlspec/core/result.py +12 -27
  49. sqlspec/core/splitter.py +17 -21
  50. sqlspec/core/statement.py +150 -159
  51. sqlspec/driver/_async.py +2 -15
  52. sqlspec/driver/_common.py +16 -95
  53. sqlspec/driver/_sync.py +2 -15
  54. sqlspec/driver/mixins/_result_tools.py +8 -29
  55. sqlspec/driver/mixins/_sql_translator.py +6 -8
  56. sqlspec/exceptions.py +1 -2
  57. sqlspec/loader.py +43 -115
  58. sqlspec/migrations/__init__.py +1 -1
  59. sqlspec/migrations/base.py +34 -45
  60. sqlspec/migrations/commands.py +34 -15
  61. sqlspec/migrations/loaders.py +1 -1
  62. sqlspec/migrations/runner.py +104 -19
  63. sqlspec/migrations/tracker.py +49 -2
  64. sqlspec/protocols.py +13 -6
  65. sqlspec/storage/__init__.py +4 -4
  66. sqlspec/storage/backends/fsspec.py +5 -6
  67. sqlspec/storage/backends/obstore.py +7 -8
  68. sqlspec/storage/registry.py +3 -3
  69. sqlspec/utils/__init__.py +2 -2
  70. sqlspec/utils/logging.py +6 -10
  71. sqlspec/utils/sync_tools.py +27 -4
  72. sqlspec/utils/text.py +6 -1
  73. {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/METADATA +1 -1
  74. sqlspec-0.18.0.dist-info/RECORD +138 -0
  75. sqlspec/builder/_ddl_utils.py +0 -103
  76. sqlspec-0.17.0.dist-info/RECORD +0 -137
  77. {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/WHEEL +0 -0
  78. {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/entry_points.txt +0 -0
  79. {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/licenses/LICENSE +0 -0
  80. {sqlspec-0.17.0.dist-info → sqlspec-0.18.0.dist-info}/licenses/NOTICE +0 -0
@@ -0,0 +1,46 @@
1
+ """Expression wrapper classes for proper type annotations."""
2
+
3
+ from typing import cast
4
+
5
+ from sqlglot import exp
6
+
7
+ __all__ = ("AggregateExpression", "ConversionExpression", "FunctionExpression", "MathExpression", "StringExpression")
8
+
9
+
10
+ class ExpressionWrapper:
11
+ """Base wrapper for SQLGlot expressions."""
12
+
13
+ def __init__(self, expression: exp.Expression) -> None:
14
+ self._expression = expression
15
+
16
+ def as_(self, alias: str) -> exp.Alias:
17
+ """Create an aliased expression."""
18
+ return cast("exp.Alias", exp.alias_(self._expression, alias))
19
+
20
+ @property
21
+ def expression(self) -> exp.Expression:
22
+ """Get the underlying SQLGlot expression."""
23
+ return self._expression
24
+
25
+ def __str__(self) -> str:
26
+ return str(self._expression)
27
+
28
+
29
+ class AggregateExpression(ExpressionWrapper):
30
+ """Aggregate functions like COUNT, SUM, AVG."""
31
+
32
+
33
+ class FunctionExpression(ExpressionWrapper):
34
+ """General SQL functions."""
35
+
36
+
37
+ class MathExpression(ExpressionWrapper):
38
+ """Mathematical functions like ROUND."""
39
+
40
+
41
+ class StringExpression(ExpressionWrapper):
42
+ """String functions like UPPER, LOWER, LENGTH."""
43
+
44
+
45
+ class ConversionExpression(ExpressionWrapper):
46
+ """Conversion functions like CAST, COALESCE."""
@@ -173,7 +173,7 @@ class Insert(QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSe
173
173
  else:
174
174
  param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
175
175
  _, param_name = self.add_parameter(value, name=param_name)
176
- value_placeholders.append(exp.var(param_name))
176
+ value_placeholders.append(exp.Placeholder(this=param_name))
177
177
 
178
178
  tuple_expr = exp.Tuple(expressions=value_placeholders)
179
179
  if self._values_added_count == 0:
@@ -412,9 +412,7 @@ class ConflictBuilder:
412
412
  # Create ON CONFLICT with proper structure
413
413
  conflict_keys = [exp.to_identifier(col) for col in self._columns] if self._columns else None
414
414
  on_conflict = exp.OnConflict(
415
- conflict_keys=conflict_keys,
416
- action=exp.var("DO UPDATE"),
417
- expressions=set_expressions if set_expressions else None,
415
+ conflict_keys=conflict_keys, action=exp.var("DO UPDATE"), expressions=set_expressions or None
418
416
  )
419
417
 
420
418
  insert_expr.set("conflict", on_conflict)
@@ -44,26 +44,26 @@ class Update(
44
44
  update_query = (
45
45
  Update()
46
46
  .table("users")
47
- .set(name="John Doe")
48
- .set(email="john@example.com")
47
+ .set_(name="John Doe")
48
+ .set_(email="john@example.com")
49
49
  .where("id = 1")
50
50
  )
51
51
 
52
52
  update_query = (
53
- Update("users").set(name="John Doe").where("id = 1")
53
+ Update("users").set_(name="John Doe").where("id = 1")
54
54
  )
55
55
 
56
56
  update_query = (
57
57
  Update()
58
58
  .table("users")
59
- .set(status="active")
59
+ .set_(status="active")
60
60
  .where_eq("id", 123)
61
61
  )
62
62
 
63
63
  update_query = (
64
64
  Update()
65
65
  .table("users", "u")
66
- .set(name="Updated Name")
66
+ .set_(name="Updated Name")
67
67
  .from_("profiles", "p")
68
68
  .where("u.id = p.user_id AND p.is_verified = true")
69
69
  )
@@ -75,14 +75,34 @@ class InsertValuesMixin:
75
75
  if not isinstance(self._expression, exp.Insert):
76
76
  msg = "Cannot set columns on a non-INSERT expression."
77
77
  raise SQLBuilderError(msg)
78
- column_exprs = [exp.column(col) if isinstance(col, str) else col for col in columns]
79
- self._expression.set("columns", column_exprs)
78
+
79
+ # Get the current table from the expression
80
+ current_this = self._expression.args.get("this")
81
+ if current_this is None:
82
+ msg = "Table must be set using .into() before setting columns."
83
+ raise SQLBuilderError(msg)
84
+
85
+ if columns:
86
+ # Create identifiers for columns
87
+ column_identifiers = [exp.to_identifier(col) if isinstance(col, str) else col for col in columns]
88
+
89
+ # Get table name from current this
90
+ table_name = current_this.this
91
+
92
+ # Create Schema object with table and columns
93
+ schema = exp.Schema(this=table_name, expressions=column_identifiers)
94
+ self._expression.set("this", schema)
95
+ # No columns specified - ensure we have just a Table object
96
+ elif isinstance(current_this, exp.Schema):
97
+ table_name = current_this.this
98
+ self._expression.set("this", exp.Table(this=table_name))
99
+
80
100
  try:
81
101
  cols = self._columns
82
102
  if not columns:
83
103
  cols.clear()
84
104
  else:
85
- cols[:] = [col.name if isinstance(col, exp.Column) else str(col) for col in columns]
105
+ cols[:] = [col if isinstance(col, str) else str(col) for col in columns]
86
106
  except AttributeError:
87
107
  pass
88
108
  return self
@@ -128,7 +148,7 @@ class InsertValuesMixin:
128
148
  column_name = column_name.split(".")[-1]
129
149
  param_name = self._generate_unique_parameter_name(column_name)
130
150
  _, param_name = self.add_parameter(val, name=param_name)
131
- row_exprs.append(exp.var(param_name))
151
+ row_exprs.append(exp.Placeholder(this=param_name))
132
152
  elif len(values) == 1 and hasattr(values[0], "items"):
133
153
  mapping = values[0]
134
154
  try:
@@ -147,7 +167,7 @@ class InsertValuesMixin:
147
167
  column_name = column_name.split(".")[-1]
148
168
  param_name = self._generate_unique_parameter_name(column_name)
149
169
  _, param_name = self.add_parameter(val, name=param_name)
150
- row_exprs.append(exp.var(param_name))
170
+ row_exprs.append(exp.Placeholder(this=param_name))
151
171
  else:
152
172
  try:
153
173
  _columns = self._columns
@@ -173,7 +193,7 @@ class InsertValuesMixin:
173
193
  except AttributeError:
174
194
  param_name = self._generate_unique_parameter_name(f"value_{i + 1}")
175
195
  _, param_name = self.add_parameter(v, name=param_name)
176
- row_exprs.append(exp.var(param_name))
196
+ row_exprs.append(exp.Placeholder(this=param_name))
177
197
 
178
198
  values_expr = exp.Values(expressions=[row_exprs])
179
199
  self._expression.set("expression", values_expr)
@@ -365,7 +365,7 @@ class MergeNotMatchedClauseMixin:
365
365
  column_name = column_name.split(".")[-1]
366
366
  param_name = self._generate_unique_parameter_name(column_name)
367
367
  param_name = self.add_parameter(val, name=param_name)[1]
368
- parameterized_values.append(exp.var(param_name))
368
+ parameterized_values.append(exp.Placeholder())
369
369
 
370
370
  insert_args["this"] = exp.Tuple(expressions=[exp.column(c) for c in columns])
371
371
  insert_args["expression"] = exp.Tuple(expressions=parameterized_values)
@@ -10,6 +10,9 @@ from sqlspec.builder._parsing_utils import parse_order_expression
10
10
  from sqlspec.exceptions import SQLBuilderError
11
11
 
12
12
  if TYPE_CHECKING:
13
+ from sqlspec.builder._column import Column
14
+ from sqlspec.builder._expression_wrappers import ExpressionWrapper
15
+ from sqlspec.builder.mixins._select_operations import Case
13
16
  from sqlspec.protocols import SQLBuilderProtocol
14
17
 
15
18
  __all__ = ("LimitOffsetClauseMixin", "OrderByClauseMixin", "ReturningClauseMixin")
@@ -24,7 +27,7 @@ class OrderByClauseMixin:
24
27
  # Type annotation for PyRight - this will be provided by the base class
25
28
  _expression: Optional[exp.Expression]
26
29
 
27
- def order_by(self, *items: Union[str, exp.Ordered], desc: bool = False) -> Self:
30
+ def order_by(self, *items: Union[str, exp.Ordered, "Column"], desc: bool = False) -> Self:
28
31
  """Add ORDER BY clause.
29
32
 
30
33
  Args:
@@ -49,7 +52,13 @@ class OrderByClauseMixin:
49
52
  if desc:
50
53
  order_item = order_item.desc()
51
54
  else:
52
- order_item = item
55
+ # Extract expression from Column objects or use as-is for sqlglot expressions
56
+ from sqlspec._sql import SQLFactory
57
+
58
+ extracted_item = SQLFactory._extract_expression(item)
59
+ order_item = extracted_item
60
+ if desc and not isinstance(item, exp.Ordered):
61
+ order_item = order_item.desc()
53
62
  current_expr = current_expr.order_by(order_item, copy=False)
54
63
  builder._expression = current_expr
55
64
  return cast("Self", builder)
@@ -111,7 +120,7 @@ class ReturningClauseMixin:
111
120
  # Type annotation for PyRight - this will be provided by the base class
112
121
  _expression: Optional[exp.Expression]
113
122
 
114
- def returning(self, *columns: Union[str, exp.Expression]) -> Self:
123
+ def returning(self, *columns: Union[str, exp.Expression, "Column", "ExpressionWrapper", "Case"]) -> Self:
115
124
  """Add RETURNING clause to the statement.
116
125
 
117
126
  Args:
@@ -130,6 +139,9 @@ class ReturningClauseMixin:
130
139
  if not isinstance(self._expression, valid_types):
131
140
  msg = "RETURNING is only supported for INSERT, UPDATE, and DELETE statements."
132
141
  raise SQLBuilderError(msg)
133
- returning_exprs = [exp.column(c) if isinstance(c, str) else c for c in columns]
142
+ # Extract expressions from various wrapper types
143
+ from sqlspec._sql import SQLFactory
144
+
145
+ returning_exprs = [SQLFactory._extract_expression(c) for c in columns]
134
146
  self._expression.set("returning", exp.Returning(expressions=returning_exprs))
135
147
  return self
@@ -1,6 +1,5 @@
1
1
  """SELECT clause mixins consolidated into a single module."""
2
2
 
3
- from dataclasses import dataclass
4
3
  from typing import TYPE_CHECKING, Any, Optional, Union, cast
5
4
 
6
5
  from mypy_extensions import trait
@@ -538,13 +537,10 @@ class SelectClauseMixin:
538
537
  return CaseBuilder(builder, alias)
539
538
 
540
539
 
541
- @dataclass
542
540
  class CaseBuilder:
543
541
  """Builder for CASE expressions."""
544
542
 
545
- _parent: "SelectBuilderProtocol"
546
- _alias: Optional[str]
547
- _case_expr: exp.Case
543
+ __slots__ = ("_alias", "_case_expr", "_parent")
548
544
 
549
545
  def __init__(self, parent: "SelectBuilderProtocol", alias: "Optional[str]" = None) -> None:
550
546
  """Initialize CaseBuilder.
@@ -858,7 +854,7 @@ class Case:
858
854
  from sqlspec._sql import SQLFactory
859
855
 
860
856
  cond_expr = exp.maybe_parse(condition) or exp.column(condition) if isinstance(condition, str) else condition
861
- val_expr = SQLFactory._to_literal(value)
857
+ val_expr = SQLFactory._to_expression(value)
862
858
 
863
859
  # SQLGlot uses exp.If for CASE WHEN clauses, not exp.When
864
860
  when_clause = exp.If(this=cond_expr, true=val_expr)
@@ -876,7 +872,7 @@ class Case:
876
872
  """
877
873
  from sqlspec._sql import SQLFactory
878
874
 
879
- self._default = SQLFactory._to_literal(value)
875
+ self._default = SQLFactory._to_expression(value)
880
876
  return self
881
877
 
882
878
  def end(self) -> Self:
@@ -111,10 +111,10 @@ class UpdateSetClauseMixin:
111
111
  """Set columns and values for the UPDATE statement.
112
112
 
113
113
  Supports:
114
- - set(column, value)
115
- - set(mapping)
116
- - set(**kwargs)
117
- - set(mapping, **kwargs)
114
+ - set_(column, value)
115
+ - set_(mapping)
116
+ - set_(**kwargs)
117
+ - set_(mapping, **kwargs)
118
118
 
119
119
  Args:
120
120
  *args: Either (column, value) or a mapping.
sqlspec/config.py CHANGED
@@ -5,6 +5,7 @@ from typing_extensions import NotRequired, TypedDict
5
5
 
6
6
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
7
7
  from sqlspec.core.statement import StatementConfig
8
+ from sqlspec.migrations.tracker import AsyncMigrationTracker, SyncMigrationTracker
8
9
  from sqlspec.utils.logging import get_logger
9
10
 
10
11
  if TYPE_CHECKING:
@@ -21,6 +22,7 @@ __all__ = (
21
22
  "DatabaseConfigProtocol",
22
23
  "DriverT",
23
24
  "LifecycleConfig",
25
+ "MigrationConfig",
24
26
  "NoPoolAsyncConfig",
25
27
  "NoPoolSyncConfig",
26
28
  "SyncConfigT",
@@ -43,7 +45,7 @@ logger = get_logger("config")
43
45
 
44
46
 
45
47
  class LifecycleConfig(TypedDict, total=False):
46
- """Universal lifecycle hooks for all adapters.
48
+ """Lifecycle hooks for all adapters.
47
49
 
48
50
  Each hook accepts a list of callables to support multiple handlers.
49
51
  """
@@ -59,6 +61,22 @@ class LifecycleConfig(TypedDict, total=False):
59
61
  on_error: NotRequired[list[Callable[[Exception, str, dict], None]]]
60
62
 
61
63
 
64
+ class MigrationConfig(TypedDict, total=False):
65
+ """Configuration options for database migrations.
66
+
67
+ All fields are optional with sensible defaults.
68
+ """
69
+
70
+ script_location: NotRequired[str]
71
+ """Path to the migrations directory. Defaults to 'migrations'."""
72
+
73
+ version_table_name: NotRequired[str]
74
+ """Name of the table used to track applied migrations. Defaults to 'sqlspec_migrations'."""
75
+
76
+ project_root: NotRequired[str]
77
+ """Path to the project root directory. Used for relative path resolution."""
78
+
79
+
62
80
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
63
81
  """Protocol defining the interface for database configurations."""
64
82
 
@@ -73,7 +91,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
73
91
  supports_native_parquet_export: "ClassVar[bool]" = False
74
92
  statement_config: "StatementConfig"
75
93
  pool_instance: "Optional[PoolT]"
76
- migration_config: "dict[str, Any]"
94
+ migration_config: "Union[dict[str, Any], MigrationConfig]"
77
95
 
78
96
  def __hash__(self) -> int:
79
97
  return id(self)
@@ -142,18 +160,19 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
142
160
  __slots__ = ("connection_config",)
143
161
  is_async: "ClassVar[bool]" = False
144
162
  supports_connection_pooling: "ClassVar[bool]" = False
163
+ migration_tracker_type: "ClassVar[type[Any]]" = SyncMigrationTracker
145
164
 
146
165
  def __init__(
147
166
  self,
148
167
  *,
149
168
  connection_config: Optional[dict[str, Any]] = None,
150
- migration_config: "Optional[dict[str, Any]]" = None,
169
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
151
170
  statement_config: "Optional[StatementConfig]" = None,
152
171
  driver_features: "Optional[dict[str, Any]]" = None,
153
172
  ) -> None:
154
173
  self.pool_instance = None
155
174
  self.connection_config = connection_config or {}
156
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
175
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
157
176
 
158
177
  if statement_config is None:
159
178
  default_parameter_config = ParameterStyleConfig(
@@ -192,21 +211,21 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
192
211
  """Base class for an async database configurations that do not implement a pool."""
193
212
 
194
213
  __slots__ = ("connection_config",)
195
-
196
214
  is_async: "ClassVar[bool]" = True
197
215
  supports_connection_pooling: "ClassVar[bool]" = False
216
+ migration_tracker_type: "ClassVar[type[Any]]" = AsyncMigrationTracker
198
217
 
199
218
  def __init__(
200
219
  self,
201
220
  *,
202
221
  connection_config: "Optional[dict[str, Any]]" = None,
203
- migration_config: "Optional[dict[str, Any]]" = None,
222
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
204
223
  statement_config: "Optional[StatementConfig]" = None,
205
224
  driver_features: "Optional[dict[str, Any]]" = None,
206
225
  ) -> None:
207
226
  self.pool_instance = None
208
227
  self.connection_config = connection_config or {}
209
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
228
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
210
229
 
211
230
  if statement_config is None:
212
231
  default_parameter_config = ParameterStyleConfig(
@@ -245,22 +264,22 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
245
264
  """Generic Sync Database Configuration."""
246
265
 
247
266
  __slots__ = ("pool_config",)
248
-
249
267
  is_async: "ClassVar[bool]" = False
250
268
  supports_connection_pooling: "ClassVar[bool]" = True
269
+ migration_tracker_type: "ClassVar[type[Any]]" = SyncMigrationTracker
251
270
 
252
271
  def __init__(
253
272
  self,
254
273
  *,
255
274
  pool_config: "Optional[dict[str, Any]]" = None,
256
275
  pool_instance: "Optional[PoolT]" = None,
257
- migration_config: "Optional[dict[str, Any]]" = None,
276
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
258
277
  statement_config: "Optional[StatementConfig]" = None,
259
278
  driver_features: "Optional[dict[str, Any]]" = None,
260
279
  ) -> None:
261
280
  self.pool_instance = pool_instance
262
281
  self.pool_config = pool_config or {}
263
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
282
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
264
283
 
265
284
  if statement_config is None:
266
285
  default_parameter_config = ParameterStyleConfig(
@@ -321,22 +340,22 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
321
340
  """Generic Async Database Configuration."""
322
341
 
323
342
  __slots__ = ("pool_config",)
324
-
325
343
  is_async: "ClassVar[bool]" = True
326
344
  supports_connection_pooling: "ClassVar[bool]" = True
345
+ migration_tracker_type: "ClassVar[type[Any]]" = AsyncMigrationTracker
327
346
 
328
347
  def __init__(
329
348
  self,
330
349
  *,
331
350
  pool_config: "Optional[dict[str, Any]]" = None,
332
351
  pool_instance: "Optional[PoolT]" = None,
333
- migration_config: "Optional[dict[str, Any]]" = None,
352
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
334
353
  statement_config: "Optional[StatementConfig]" = None,
335
354
  driver_features: "Optional[dict[str, Any]]" = None,
336
355
  ) -> None:
337
356
  self.pool_instance = pool_instance
338
357
  self.pool_config = pool_config or {}
339
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
358
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
340
359
 
341
360
  if statement_config is None:
342
361
  self.statement_config = StatementConfig(
sqlspec/core/__init__.py CHANGED
@@ -1,17 +1,92 @@
1
- """SQLSpec Core Module - SQL Processing System.
2
-
3
- This module provides the core SQL processing components including statement handling,
4
- parameter processing, compilation, and result management.
5
-
6
- Components:
7
- - statement.py: SQL class with StatementConfig
8
- - parameters.py: Parameter processing pipeline
9
- - compiler.py: SQL compilation with caching
10
- - result.py: Result classes for query execution
11
- - filters.py: Statement filter system
12
- - cache.py: Unified caching system
13
- - splitter.py: SQL statement splitter
14
- - hashing.py: Cache key generation
1
+ """SQLSpec Core Module - High-Performance SQL Processing System.
2
+
3
+ This module provides the core SQL processing infrastructure for SQLSpec, implementing
4
+ a complete pipeline for SQL statement compilation, parameter processing, caching,
5
+ and result management. All components are optimized for MyPyC compilation and
6
+ designed for maximum performance with minimal overhead.
7
+
8
+ Architecture Overview:
9
+ The core module implements a single-pass processing pipeline where SQL statements
10
+ are parsed once, transformed once, and validated once. The SQL object serves as
11
+ the single source of truth throughout the system.
12
+
13
+ Key Components:
14
+ statement.py: SQL statement representation and configuration management
15
+ - SQL class for statement encapsulation with lazy compilation
16
+ - StatementConfig for processing pipeline configuration
17
+ - ProcessedState for cached compilation results
18
+ - Support for execute_many and script execution modes
19
+
20
+ parameters.py: Type-safe parameter processing and style conversion
21
+ - Automatic parameter style detection and conversion
22
+ - Support for QMARK (?), NAMED (:name), NUMERIC ($1), FORMAT (%s) styles
23
+ - Parameter validation and type coercion
24
+ - Batch parameter handling for execute_many operations
25
+
26
+ compiler.py: SQL compilation with validation and optimization
27
+ - SQLProcessor for statement compilation and validation
28
+ - Operation type detection (SELECT, INSERT, UPDATE, DELETE, etc.)
29
+ - AST-based SQL analysis using SQLGlot
30
+ - Support for multiple SQL dialects
31
+ - Compiled result caching for performance
32
+
33
+ result.py: Comprehensive result handling for all SQL operations
34
+ - SQLResult for standard query results with metadata
35
+ - ArrowResult for Apache Arrow format integration
36
+ - Support for DML operations with RETURNING clauses
37
+ - Script execution result aggregation
38
+ - Iterator protocol support for result rows
39
+
40
+ filters.py: Composable SQL statement filters
41
+ - BeforeAfterFilter for date range filtering
42
+ - InCollectionFilter for IN clause generation
43
+ - LimitOffsetFilter for pagination
44
+ - OrderByFilter for dynamic sorting
45
+ - SearchFilter for text search operations
46
+ - Parameter conflict resolution
47
+
48
+ cache.py: Unified caching system with LRU eviction
49
+ - UnifiedCache with configurable TTL and size limits
50
+ - StatementCache for compiled SQL statements
51
+ - ExpressionCache for parsed SQLGlot expressions
52
+ - ParameterCache for processed parameters
53
+ - Thread-safe operations with fine-grained locking
54
+ - Cache statistics and monitoring
55
+
56
+ splitter.py: Dialect-aware SQL script splitting
57
+ - Support for Oracle PL/SQL, T-SQL, PostgreSQL, MySQL
58
+ - Proper handling of block structures (BEGIN/END)
59
+ - Dollar-quoted string support for PostgreSQL
60
+ - Batch separator recognition (GO for T-SQL)
61
+ - Comment and string literal preservation
62
+
63
+ hashing.py: Efficient cache key generation
64
+ - SQL statement hashing with parameter consideration
65
+ - Expression tree hashing for AST caching
66
+ - Parameter set hashing for batch operations
67
+ - Optimized hash computation with caching
68
+
69
+ Performance Optimizations:
70
+ - MyPyC compilation support with proper annotations
71
+ - __slots__ usage for memory efficiency
72
+ - Final annotations for constant folding
73
+ - Lazy evaluation and compilation
74
+ - Comprehensive result caching
75
+ - Minimal object allocation in hot paths
76
+
77
+ Thread Safety:
78
+ All caching components are thread-safe with RLock protection.
79
+ The processing pipeline is stateless and safe for concurrent use.
80
+
81
+ Example Usage:
82
+ >>> from sqlspec.core import SQL, StatementConfig
83
+ >>> config = StatementConfig(dialect="postgresql")
84
+ >>> stmt = SQL(
85
+ ... "SELECT * FROM users WHERE id = ?",
86
+ ... 1,
87
+ ... statement_config=config,
88
+ ... )
89
+ >>> compiled_sql, params = stmt.compile()
15
90
  """
16
91
 
17
92
  from sqlspec.core import filters