sqlspec 0.18.0__py3-none-any.whl → 0.20.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 (64) hide show
  1. sqlspec/adapters/adbc/driver.py +192 -28
  2. sqlspec/adapters/asyncmy/driver.py +72 -15
  3. sqlspec/adapters/asyncpg/config.py +23 -3
  4. sqlspec/adapters/asyncpg/driver.py +30 -14
  5. sqlspec/adapters/bigquery/driver.py +79 -9
  6. sqlspec/adapters/duckdb/driver.py +39 -56
  7. sqlspec/adapters/oracledb/driver.py +99 -52
  8. sqlspec/adapters/psqlpy/driver.py +89 -31
  9. sqlspec/adapters/psycopg/driver.py +11 -23
  10. sqlspec/adapters/sqlite/driver.py +77 -8
  11. sqlspec/base.py +29 -25
  12. sqlspec/builder/__init__.py +1 -1
  13. sqlspec/builder/_base.py +4 -5
  14. sqlspec/builder/_column.py +3 -3
  15. sqlspec/builder/_ddl.py +5 -1
  16. sqlspec/builder/_delete.py +5 -6
  17. sqlspec/builder/_insert.py +6 -7
  18. sqlspec/builder/_merge.py +5 -5
  19. sqlspec/builder/_parsing_utils.py +3 -3
  20. sqlspec/builder/_select.py +6 -5
  21. sqlspec/builder/_update.py +4 -5
  22. sqlspec/builder/mixins/_cte_and_set_ops.py +5 -1
  23. sqlspec/builder/mixins/_delete_operations.py +5 -1
  24. sqlspec/builder/mixins/_insert_operations.py +5 -1
  25. sqlspec/builder/mixins/_join_operations.py +5 -0
  26. sqlspec/builder/mixins/_merge_operations.py +5 -1
  27. sqlspec/builder/mixins/_order_limit_operations.py +5 -1
  28. sqlspec/builder/mixins/_pivot_operations.py +4 -1
  29. sqlspec/builder/mixins/_select_operations.py +5 -1
  30. sqlspec/builder/mixins/_update_operations.py +5 -1
  31. sqlspec/builder/mixins/_where_clause.py +5 -1
  32. sqlspec/cli.py +281 -33
  33. sqlspec/config.py +160 -10
  34. sqlspec/core/compiler.py +11 -3
  35. sqlspec/core/filters.py +30 -9
  36. sqlspec/core/parameters.py +67 -67
  37. sqlspec/core/result.py +62 -31
  38. sqlspec/core/splitter.py +160 -34
  39. sqlspec/core/statement.py +95 -14
  40. sqlspec/driver/_common.py +12 -3
  41. sqlspec/driver/mixins/_result_tools.py +21 -4
  42. sqlspec/driver/mixins/_sql_translator.py +45 -7
  43. sqlspec/extensions/aiosql/adapter.py +1 -1
  44. sqlspec/extensions/litestar/_utils.py +1 -1
  45. sqlspec/extensions/litestar/handlers.py +21 -0
  46. sqlspec/extensions/litestar/plugin.py +15 -8
  47. sqlspec/loader.py +12 -12
  48. sqlspec/migrations/loaders.py +5 -2
  49. sqlspec/migrations/utils.py +2 -2
  50. sqlspec/storage/backends/obstore.py +1 -3
  51. sqlspec/storage/registry.py +1 -1
  52. sqlspec/utils/__init__.py +7 -0
  53. sqlspec/utils/deprecation.py +6 -0
  54. sqlspec/utils/fixtures.py +239 -30
  55. sqlspec/utils/module_loader.py +5 -1
  56. sqlspec/utils/serializers.py +6 -0
  57. sqlspec/utils/singleton.py +6 -0
  58. sqlspec/utils/sync_tools.py +10 -1
  59. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/METADATA +1 -1
  60. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/RECORD +64 -64
  61. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/WHEEL +0 -0
  62. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/entry_points.txt +0 -0
  63. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/licenses/LICENSE +0 -0
  64. {sqlspec-0.18.0.dist-info → sqlspec-0.20.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/config.py CHANGED
@@ -11,8 +11,11 @@ from sqlspec.utils.logging import get_logger
11
11
  if TYPE_CHECKING:
12
12
  from collections.abc import Awaitable
13
13
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
14
+ from pathlib import Path
14
15
 
15
16
  from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
17
+ from sqlspec.loader import SQLFileLoader
18
+ from sqlspec.migrations.commands import MigrationCommands
16
19
 
17
20
 
18
21
  __all__ = (
@@ -45,7 +48,7 @@ logger = get_logger("config")
45
48
 
46
49
 
47
50
  class LifecycleConfig(TypedDict, total=False):
48
- """Lifecycle hooks for all adapters.
51
+ """Lifecycle hooks for database adapters.
49
52
 
50
53
  Each hook accepts a list of callables to support multiple handlers.
51
54
  """
@@ -64,7 +67,7 @@ class LifecycleConfig(TypedDict, total=False):
64
67
  class MigrationConfig(TypedDict, total=False):
65
68
  """Configuration options for database migrations.
66
69
 
67
- All fields are optional with sensible defaults.
70
+ All fields are optional with default values.
68
71
  """
69
72
 
70
73
  script_location: NotRequired[str]
@@ -76,11 +79,24 @@ class MigrationConfig(TypedDict, total=False):
76
79
  project_root: NotRequired[str]
77
80
  """Path to the project root directory. Used for relative path resolution."""
78
81
 
82
+ enabled: NotRequired[bool]
83
+ """Whether this configuration should be included in CLI operations. Defaults to True."""
84
+
79
85
 
80
86
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
81
87
  """Protocol defining the interface for database configurations."""
82
88
 
83
- __slots__ = ("driver_features", "migration_config", "pool_instance", "statement_config")
89
+ __slots__ = (
90
+ "_migration_commands",
91
+ "_migration_loader",
92
+ "driver_features",
93
+ "migration_config",
94
+ "pool_instance",
95
+ "statement_config",
96
+ )
97
+
98
+ _migration_loader: "SQLFileLoader"
99
+ _migration_commands: "MigrationCommands"
84
100
  driver_type: "ClassVar[type[Any]]"
85
101
  connection_type: "ClassVar[type[Any]]"
86
102
  is_async: "ClassVar[bool]" = False
@@ -144,18 +160,148 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
144
160
  def get_signature_namespace(self) -> "dict[str, type[Any]]":
145
161
  """Get the signature namespace for this database configuration.
146
162
 
147
- This method returns a dictionary of type names to types that should be
148
- registered with Litestar's signature namespace to prevent serialization
149
- attempts on database-specific types.
163
+ Returns a dictionary of type names to types that should be registered
164
+ with Litestar's signature namespace to prevent serialization attempts
165
+ on database-specific types.
150
166
 
151
167
  Returns:
152
168
  Dictionary mapping type names to types.
153
169
  """
154
170
  return {}
155
171
 
172
+ def _initialize_migration_components(self) -> None:
173
+ """Initialize migration loader and commands with necessary imports.
174
+
175
+ Handles the circular import between config and commands by importing
176
+ at runtime when needed.
177
+ """
178
+ from sqlspec.loader import SQLFileLoader
179
+ from sqlspec.migrations.commands import MigrationCommands
180
+
181
+ self._migration_loader = SQLFileLoader()
182
+ self._migration_commands = MigrationCommands(self) # type: ignore[arg-type]
183
+
184
+ def _ensure_migration_loader(self) -> "SQLFileLoader":
185
+ """Get the migration SQL loader and auto-load files if needed.
186
+
187
+ Returns:
188
+ SQLFileLoader instance for migration files.
189
+ """
190
+ # Auto-load migration files from configured migration path if it exists
191
+ migration_config = self.migration_config or {}
192
+ script_location = migration_config.get("script_location", "migrations")
193
+
194
+ from pathlib import Path
195
+
196
+ migration_path = Path(script_location)
197
+ if migration_path.exists() and not self._migration_loader.list_files():
198
+ self._migration_loader.load_sql(migration_path)
199
+ logger.debug("Auto-loaded migration SQL files from %s", migration_path)
200
+
201
+ return self._migration_loader
202
+
203
+ def _ensure_migration_commands(self) -> "MigrationCommands":
204
+ """Get the migration commands instance.
205
+
206
+ Returns:
207
+ MigrationCommands instance for this config.
208
+ """
209
+ return self._migration_commands
210
+
211
+ def get_migration_loader(self) -> "SQLFileLoader":
212
+ """Get the SQL loader for migration files.
213
+
214
+ Provides access to migration SQL files loaded from the configured
215
+ script_location directory. Files are loaded lazily on first access.
216
+
217
+ Returns:
218
+ SQLFileLoader instance with migration files loaded.
219
+ """
220
+ return self._ensure_migration_loader()
221
+
222
+ def load_migration_sql_files(self, *paths: "Union[str, Path]") -> None:
223
+ """Load additional migration SQL files from specified paths.
224
+
225
+ Args:
226
+ *paths: One or more file paths or directory paths to load migration SQL files from.
227
+ """
228
+ from pathlib import Path
229
+
230
+ loader = self._ensure_migration_loader()
231
+ for path in paths:
232
+ path_obj = Path(path)
233
+ if path_obj.exists():
234
+ loader.load_sql(path_obj)
235
+ logger.debug("Loaded migration SQL files from %s", path_obj)
236
+ else:
237
+ logger.warning("Migration path does not exist: %s", path_obj)
238
+
239
+ def get_migration_commands(self) -> "MigrationCommands":
240
+ """Get migration commands for this configuration.
241
+
242
+ Returns:
243
+ MigrationCommands instance configured for this database.
244
+ """
245
+ return self._ensure_migration_commands()
246
+
247
+ def migrate_up(self, revision: str = "head") -> None:
248
+ """Apply migrations up to the specified revision.
249
+
250
+ Args:
251
+ revision: Target revision or "head" for latest. Defaults to "head".
252
+ """
253
+ commands = self._ensure_migration_commands()
254
+ commands.upgrade(revision)
255
+
256
+ def migrate_down(self, revision: str = "-1") -> None:
257
+ """Apply migrations down to the specified revision.
258
+
259
+ Args:
260
+ revision: Target revision, "-1" for one step back, or "base" for all migrations. Defaults to "-1".
261
+ """
262
+ commands = self._ensure_migration_commands()
263
+ commands.downgrade(revision)
264
+
265
+ def get_current_migration(self, verbose: bool = False) -> "Optional[str]":
266
+ """Get the current migration version.
267
+
268
+ Args:
269
+ verbose: Whether to show detailed migration history.
270
+
271
+ Returns:
272
+ Current migration version or None if no migrations applied.
273
+ """
274
+ commands = self._ensure_migration_commands()
275
+ return commands.current(verbose=verbose)
276
+
277
+ def create_migration(self, message: str, file_type: str = "sql") -> None:
278
+ """Create a new migration file.
279
+
280
+ Args:
281
+ message: Description for the migration.
282
+ file_type: Type of migration file to create ('sql' or 'py'). Defaults to 'sql'.
283
+ """
284
+ commands = self._ensure_migration_commands()
285
+ commands.revision(message, file_type)
286
+
287
+ def init_migrations(self, directory: "Optional[str]" = None, package: bool = True) -> None:
288
+ """Initialize migration directory structure.
289
+
290
+ Args:
291
+ directory: Directory to initialize migrations in. Uses script_location from migration_config if not provided.
292
+ package: Whether to create __init__.py file. Defaults to True.
293
+ """
294
+ if directory is None:
295
+ migration_config = self.migration_config or {}
296
+ directory = migration_config.get("script_location") or "migrations"
297
+
298
+ commands = self._ensure_migration_commands()
299
+ assert directory is not None
300
+ commands.init(directory, package)
301
+
156
302
 
157
303
  class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
158
- """Base class for a sync database configurations that do not implement a pool."""
304
+ """Base class for sync database configurations that do not implement a pool."""
159
305
 
160
306
  __slots__ = ("connection_config",)
161
307
  is_async: "ClassVar[bool]" = False
@@ -173,6 +319,7 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
173
319
  self.pool_instance = None
174
320
  self.connection_config = connection_config or {}
175
321
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
322
+ self._initialize_migration_components()
176
323
 
177
324
  if statement_config is None:
178
325
  default_parameter_config = ParameterStyleConfig(
@@ -208,7 +355,7 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
208
355
 
209
356
 
210
357
  class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
211
- """Base class for an async database configurations that do not implement a pool."""
358
+ """Base class for async database configurations that do not implement a pool."""
212
359
 
213
360
  __slots__ = ("connection_config",)
214
361
  is_async: "ClassVar[bool]" = True
@@ -226,6 +373,7 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
226
373
  self.pool_instance = None
227
374
  self.connection_config = connection_config or {}
228
375
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
376
+ self._initialize_migration_components()
229
377
 
230
378
  if statement_config is None:
231
379
  default_parameter_config = ParameterStyleConfig(
@@ -261,7 +409,7 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
261
409
 
262
410
 
263
411
  class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
264
- """Generic Sync Database Configuration."""
412
+ """Base class for sync database configurations with connection pooling."""
265
413
 
266
414
  __slots__ = ("pool_config",)
267
415
  is_async: "ClassVar[bool]" = False
@@ -280,6 +428,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
280
428
  self.pool_instance = pool_instance
281
429
  self.pool_config = pool_config or {}
282
430
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
431
+ self._initialize_migration_components()
283
432
 
284
433
  if statement_config is None:
285
434
  default_parameter_config = ParameterStyleConfig(
@@ -337,7 +486,7 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
337
486
 
338
487
 
339
488
  class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
340
- """Generic Async Database Configuration."""
489
+ """Base class for async database configurations with connection pooling."""
341
490
 
342
491
  __slots__ = ("pool_config",)
343
492
  is_async: "ClassVar[bool]" = True
@@ -356,6 +505,7 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
356
505
  self.pool_instance = pool_instance
357
506
  self.pool_config = pool_config or {}
358
507
  self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
508
+ self._initialize_migration_components()
359
509
 
360
510
  if statement_config is None:
361
511
  self.statement_config = StatementConfig(
sqlspec/core/compiler.py CHANGED
@@ -17,6 +17,7 @@ from sqlglot.errors import ParseError
17
17
  from typing_extensions import Literal
18
18
 
19
19
  from sqlspec.core.parameters import ParameterProcessor
20
+ from sqlspec.exceptions import SQLSpecError
20
21
  from sqlspec.utils.logging import get_logger
21
22
 
22
23
  if TYPE_CHECKING:
@@ -61,6 +62,8 @@ class CompiledSQL:
61
62
  """Compiled SQL result.
62
63
 
63
64
  Contains the result of SQL compilation with information needed for execution.
65
+ Immutable container holding compiled SQL text, processed parameters, operation
66
+ type, and execution metadata.
64
67
  """
65
68
 
66
69
  __slots__ = (
@@ -133,7 +136,9 @@ class CompiledSQL:
133
136
  class SQLProcessor:
134
137
  """SQL processor with compilation and caching.
135
138
 
136
- Processes SQL statements with parameter processing and caching.
139
+ Processes SQL statements by compiling them into executable format with
140
+ parameter substitution. Includes LRU-style caching for compilation results
141
+ to avoid re-processing identical statements.
137
142
  """
138
143
 
139
144
  __slots__ = ("_cache", "_cache_hits", "_cache_misses", "_config", "_max_cache_size", "_parameter_processor")
@@ -157,7 +162,7 @@ class SQLProcessor:
157
162
 
158
163
  Args:
159
164
  sql: SQL string for compilation
160
- parameters: Parameter values
165
+ parameters: Parameter values for substitution
161
166
  is_many: Whether this is for execute_many operation
162
167
 
163
168
  Returns:
@@ -261,6 +266,9 @@ class SQLProcessor:
261
266
  supports_many=isinstance(final_params, list) and len(final_params) > 0,
262
267
  )
263
268
 
269
+ except SQLSpecError:
270
+ # Re-raise SQLSpecError (validation errors, parameter mismatches) - these should fail hard
271
+ raise
264
272
  except Exception as e:
265
273
  logger.warning("Compilation failed, using fallback: %s", e)
266
274
  return CompiledSQL(compiled_sql=sql, execution_parameters=parameters, operation_type="UNKNOWN")
@@ -340,7 +348,7 @@ class SQLProcessor:
340
348
  return sql, parameters
341
349
 
342
350
  def clear_cache(self) -> None:
343
- """Clear cache."""
351
+ """Clear compilation cache and reset statistics."""
344
352
  self._cache.clear()
345
353
  self._cache_hits = 0
346
354
  self._cache_misses = 0
sqlspec/core/filters.py CHANGED
@@ -196,7 +196,10 @@ class BeforeAfterFilter(StatementFilter):
196
196
 
197
197
 
198
198
  class OnBeforeAfterFilter(StatementFilter):
199
- """Data required to filter a query on a ``datetime`` column."""
199
+ """Filter for inclusive datetime range queries.
200
+
201
+ Applies WHERE clauses for on-or-before/on-or-after datetime filtering.
202
+ """
200
203
 
201
204
  __slots__ = ("_param_name_on_or_after", "_param_name_on_or_before", "field_name", "on_or_after", "on_or_before")
202
205
 
@@ -277,7 +280,7 @@ class OnBeforeAfterFilter(StatementFilter):
277
280
 
278
281
 
279
282
  class InAnyFilter(StatementFilter, ABC, Generic[T]):
280
- """Subclass for methods that have a `prefer_any` attribute."""
283
+ """Base class for collection-based filters that support ANY operations."""
281
284
 
282
285
  __slots__ = ()
283
286
 
@@ -346,7 +349,10 @@ class InCollectionFilter(InAnyFilter[T]):
346
349
 
347
350
 
348
351
  class NotInCollectionFilter(InAnyFilter[T]):
349
- """Data required to construct a ``WHERE ... NOT IN (...)`` clause."""
352
+ """Filter for NOT IN clause queries.
353
+
354
+ Constructs WHERE ... NOT IN (...) clauses.
355
+ """
350
356
 
351
357
  __slots__ = ("_param_names", "field_name", "values")
352
358
 
@@ -401,7 +407,10 @@ class NotInCollectionFilter(InAnyFilter[T]):
401
407
 
402
408
 
403
409
  class AnyCollectionFilter(InAnyFilter[T]):
404
- """Data required to construct a ``WHERE column_name = ANY (array_expression)`` clause."""
410
+ """Filter for PostgreSQL-style ANY clause queries.
411
+
412
+ Constructs WHERE column_name = ANY (array_expression) clauses.
413
+ """
405
414
 
406
415
  __slots__ = ("_param_names", "field_name", "values")
407
416
 
@@ -460,7 +469,10 @@ class AnyCollectionFilter(InAnyFilter[T]):
460
469
 
461
470
 
462
471
  class NotAnyCollectionFilter(InAnyFilter[T]):
463
- """Data required to construct a ``WHERE NOT (column_name = ANY (array_expression))`` clause."""
472
+ """Filter for PostgreSQL-style NOT ANY clause queries.
473
+
474
+ Constructs WHERE NOT (column_name = ANY (array_expression)) clauses.
475
+ """
464
476
 
465
477
  __slots__ = ("_param_names", "field_name", "values")
466
478
 
@@ -514,7 +526,7 @@ class NotAnyCollectionFilter(InAnyFilter[T]):
514
526
 
515
527
 
516
528
  class PaginationFilter(StatementFilter, ABC):
517
- """Subclass for methods that function as a pagination type."""
529
+ """Base class for pagination-related filters."""
518
530
 
519
531
  __slots__ = ()
520
532
 
@@ -524,7 +536,10 @@ class PaginationFilter(StatementFilter, ABC):
524
536
 
525
537
 
526
538
  class LimitOffsetFilter(PaginationFilter):
527
- """Data required to add limit/offset filtering to a query."""
539
+ """Filter for LIMIT and OFFSET clauses.
540
+
541
+ Adds pagination support through LIMIT/OFFSET SQL clauses.
542
+ """
528
543
 
529
544
  __slots__ = ("_limit_param_name", "_offset_param_name", "limit", "offset")
530
545
 
@@ -576,7 +591,10 @@ class LimitOffsetFilter(PaginationFilter):
576
591
 
577
592
 
578
593
  class OrderByFilter(StatementFilter):
579
- """Data required to construct a ``ORDER BY ...`` clause."""
594
+ """Filter for ORDER BY clauses.
595
+
596
+ Adds sorting capability to SQL queries.
597
+ """
580
598
 
581
599
  __slots__ = ("field_name", "sort_order")
582
600
 
@@ -694,7 +712,10 @@ class SearchFilter(StatementFilter):
694
712
 
695
713
 
696
714
  class NotInSearchFilter(SearchFilter):
697
- """Data required to construct a ``WHERE field_name NOT LIKE '%' || :value || '%'`` clause."""
715
+ """Filter for negated text search queries.
716
+
717
+ Constructs WHERE field_name NOT LIKE '%value%' clauses.
718
+ """
698
719
 
699
720
  __slots__ = ()
700
721