sqlspec 0.17.1__py3-none-any.whl → 0.19.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 (77) hide show
  1. sqlspec/__init__.py +1 -1
  2. sqlspec/_sql.py +54 -159
  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 +19 -22
  29. sqlspec/builder/__init__.py +1 -1
  30. sqlspec/builder/_base.py +34 -20
  31. sqlspec/builder/_ddl.py +407 -183
  32. sqlspec/builder/_insert.py +1 -1
  33. sqlspec/builder/mixins/_insert_operations.py +26 -6
  34. sqlspec/builder/mixins/_merge_operations.py +1 -1
  35. sqlspec/builder/mixins/_select_operations.py +1 -5
  36. sqlspec/cli.py +281 -33
  37. sqlspec/config.py +183 -14
  38. sqlspec/core/__init__.py +89 -14
  39. sqlspec/core/cache.py +57 -104
  40. sqlspec/core/compiler.py +57 -112
  41. sqlspec/core/filters.py +1 -21
  42. sqlspec/core/hashing.py +13 -47
  43. sqlspec/core/parameters.py +272 -261
  44. sqlspec/core/result.py +12 -27
  45. sqlspec/core/splitter.py +17 -21
  46. sqlspec/core/statement.py +150 -159
  47. sqlspec/driver/_async.py +2 -15
  48. sqlspec/driver/_common.py +16 -95
  49. sqlspec/driver/_sync.py +2 -15
  50. sqlspec/driver/mixins/_result_tools.py +8 -29
  51. sqlspec/driver/mixins/_sql_translator.py +6 -8
  52. sqlspec/exceptions.py +1 -2
  53. sqlspec/extensions/litestar/plugin.py +15 -8
  54. sqlspec/loader.py +43 -115
  55. sqlspec/migrations/__init__.py +1 -1
  56. sqlspec/migrations/base.py +34 -45
  57. sqlspec/migrations/commands.py +34 -15
  58. sqlspec/migrations/loaders.py +1 -1
  59. sqlspec/migrations/runner.py +104 -19
  60. sqlspec/migrations/tracker.py +49 -2
  61. sqlspec/protocols.py +3 -6
  62. sqlspec/storage/__init__.py +4 -4
  63. sqlspec/storage/backends/fsspec.py +5 -6
  64. sqlspec/storage/backends/obstore.py +7 -8
  65. sqlspec/storage/registry.py +3 -3
  66. sqlspec/utils/__init__.py +2 -2
  67. sqlspec/utils/logging.py +6 -10
  68. sqlspec/utils/sync_tools.py +27 -4
  69. sqlspec/utils/text.py +6 -1
  70. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/METADATA +1 -1
  71. sqlspec-0.19.0.dist-info/RECORD +138 -0
  72. sqlspec/builder/_ddl_utils.py +0 -103
  73. sqlspec-0.17.1.dist-info/RECORD +0 -138
  74. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/WHEEL +0 -0
  75. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/entry_points.txt +0 -0
  76. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/LICENSE +0 -0
  77. {sqlspec-0.17.1.dist-info → sqlspec-0.19.0.dist-info}/licenses/NOTICE +0 -0
sqlspec/config.py CHANGED
@@ -5,13 +5,17 @@ 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:
11
12
  from collections.abc import Awaitable
12
13
  from contextlib import AbstractAsyncContextManager, AbstractContextManager
14
+ from pathlib import Path
13
15
 
14
16
  from sqlspec.driver import AsyncDriverAdapterBase, SyncDriverAdapterBase
17
+ from sqlspec.loader import SQLFileLoader
18
+ from sqlspec.migrations.commands import MigrationCommands
15
19
 
16
20
 
17
21
  __all__ = (
@@ -21,6 +25,7 @@ __all__ = (
21
25
  "DatabaseConfigProtocol",
22
26
  "DriverT",
23
27
  "LifecycleConfig",
28
+ "MigrationConfig",
24
29
  "NoPoolAsyncConfig",
25
30
  "NoPoolSyncConfig",
26
31
  "SyncConfigT",
@@ -43,7 +48,7 @@ logger = get_logger("config")
43
48
 
44
49
 
45
50
  class LifecycleConfig(TypedDict, total=False):
46
- """Universal lifecycle hooks for all adapters.
51
+ """Lifecycle hooks for all adapters.
47
52
 
48
53
  Each hook accepts a list of callables to support multiple handlers.
49
54
  """
@@ -59,10 +64,39 @@ class LifecycleConfig(TypedDict, total=False):
59
64
  on_error: NotRequired[list[Callable[[Exception, str, dict], None]]]
60
65
 
61
66
 
67
+ class MigrationConfig(TypedDict, total=False):
68
+ """Configuration options for database migrations.
69
+
70
+ All fields are optional with sensible defaults.
71
+ """
72
+
73
+ script_location: NotRequired[str]
74
+ """Path to the migrations directory. Defaults to 'migrations'."""
75
+
76
+ version_table_name: NotRequired[str]
77
+ """Name of the table used to track applied migrations. Defaults to 'sqlspec_migrations'."""
78
+
79
+ project_root: NotRequired[str]
80
+ """Path to the project root directory. Used for relative path resolution."""
81
+
82
+ enabled: NotRequired[bool]
83
+ """Whether this configuration should be included in CLI operations. Defaults to True."""
84
+
85
+
62
86
  class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
63
87
  """Protocol defining the interface for database configurations."""
64
88
 
65
- __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"
66
100
  driver_type: "ClassVar[type[Any]]"
67
101
  connection_type: "ClassVar[type[Any]]"
68
102
  is_async: "ClassVar[bool]" = False
@@ -73,7 +107,7 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
73
107
  supports_native_parquet_export: "ClassVar[bool]" = False
74
108
  statement_config: "StatementConfig"
75
109
  pool_instance: "Optional[PoolT]"
76
- migration_config: "dict[str, Any]"
110
+ migration_config: "Union[dict[str, Any], MigrationConfig]"
77
111
 
78
112
  def __hash__(self) -> int:
79
113
  return id(self)
@@ -135,6 +169,136 @@ class DatabaseConfigProtocol(ABC, Generic[ConnectionT, PoolT, DriverT]):
135
169
  """
136
170
  return {}
137
171
 
172
+ def _initialize_migration_components(self) -> None:
173
+ """Initialize migration loader and commands with necessary imports.
174
+
175
+ This method handles the circular import between config and commands
176
+ by importing 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
+ The 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
+ The 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
+ This 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
+ The 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
+
138
302
 
139
303
  class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
140
304
  """Base class for a sync database configurations that do not implement a pool."""
@@ -142,18 +306,20 @@ class NoPoolSyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
142
306
  __slots__ = ("connection_config",)
143
307
  is_async: "ClassVar[bool]" = False
144
308
  supports_connection_pooling: "ClassVar[bool]" = False
309
+ migration_tracker_type: "ClassVar[type[Any]]" = SyncMigrationTracker
145
310
 
146
311
  def __init__(
147
312
  self,
148
313
  *,
149
314
  connection_config: Optional[dict[str, Any]] = None,
150
- migration_config: "Optional[dict[str, Any]]" = None,
315
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
151
316
  statement_config: "Optional[StatementConfig]" = None,
152
317
  driver_features: "Optional[dict[str, Any]]" = None,
153
318
  ) -> None:
154
319
  self.pool_instance = None
155
320
  self.connection_config = connection_config or {}
156
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
321
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
322
+ self._initialize_migration_components()
157
323
 
158
324
  if statement_config is None:
159
325
  default_parameter_config = ParameterStyleConfig(
@@ -192,21 +358,22 @@ class NoPoolAsyncConfig(DatabaseConfigProtocol[ConnectionT, None, DriverT]):
192
358
  """Base class for an async database configurations that do not implement a pool."""
193
359
 
194
360
  __slots__ = ("connection_config",)
195
-
196
361
  is_async: "ClassVar[bool]" = True
197
362
  supports_connection_pooling: "ClassVar[bool]" = False
363
+ migration_tracker_type: "ClassVar[type[Any]]" = AsyncMigrationTracker
198
364
 
199
365
  def __init__(
200
366
  self,
201
367
  *,
202
368
  connection_config: "Optional[dict[str, Any]]" = None,
203
- migration_config: "Optional[dict[str, Any]]" = None,
369
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
204
370
  statement_config: "Optional[StatementConfig]" = None,
205
371
  driver_features: "Optional[dict[str, Any]]" = None,
206
372
  ) -> None:
207
373
  self.pool_instance = None
208
374
  self.connection_config = connection_config or {}
209
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
375
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
376
+ self._initialize_migration_components()
210
377
 
211
378
  if statement_config is None:
212
379
  default_parameter_config = ParameterStyleConfig(
@@ -245,22 +412,23 @@ class SyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
245
412
  """Generic Sync Database Configuration."""
246
413
 
247
414
  __slots__ = ("pool_config",)
248
-
249
415
  is_async: "ClassVar[bool]" = False
250
416
  supports_connection_pooling: "ClassVar[bool]" = True
417
+ migration_tracker_type: "ClassVar[type[Any]]" = SyncMigrationTracker
251
418
 
252
419
  def __init__(
253
420
  self,
254
421
  *,
255
422
  pool_config: "Optional[dict[str, Any]]" = None,
256
423
  pool_instance: "Optional[PoolT]" = None,
257
- migration_config: "Optional[dict[str, Any]]" = None,
424
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
258
425
  statement_config: "Optional[StatementConfig]" = None,
259
426
  driver_features: "Optional[dict[str, Any]]" = None,
260
427
  ) -> None:
261
428
  self.pool_instance = pool_instance
262
429
  self.pool_config = pool_config or {}
263
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
430
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
431
+ self._initialize_migration_components()
264
432
 
265
433
  if statement_config is None:
266
434
  default_parameter_config = ParameterStyleConfig(
@@ -321,22 +489,23 @@ class AsyncDatabaseConfig(DatabaseConfigProtocol[ConnectionT, PoolT, DriverT]):
321
489
  """Generic Async Database Configuration."""
322
490
 
323
491
  __slots__ = ("pool_config",)
324
-
325
492
  is_async: "ClassVar[bool]" = True
326
493
  supports_connection_pooling: "ClassVar[bool]" = True
494
+ migration_tracker_type: "ClassVar[type[Any]]" = AsyncMigrationTracker
327
495
 
328
496
  def __init__(
329
497
  self,
330
498
  *,
331
499
  pool_config: "Optional[dict[str, Any]]" = None,
332
500
  pool_instance: "Optional[PoolT]" = None,
333
- migration_config: "Optional[dict[str, Any]]" = None,
501
+ migration_config: "Optional[Union[dict[str, Any], MigrationConfig]]" = None,
334
502
  statement_config: "Optional[StatementConfig]" = None,
335
503
  driver_features: "Optional[dict[str, Any]]" = None,
336
504
  ) -> None:
337
505
  self.pool_instance = pool_instance
338
506
  self.pool_config = pool_config or {}
339
- self.migration_config: dict[str, Any] = migration_config if migration_config is not None else {}
507
+ self.migration_config: Union[dict[str, Any], MigrationConfig] = migration_config or {}
508
+ self._initialize_migration_components()
340
509
 
341
510
  if statement_config is None:
342
511
  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