sqlspec 0.26.0__py3-none-any.whl → 0.27.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 (197) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +55 -25
  3. sqlspec/_typing.py +62 -52
  4. sqlspec/adapters/adbc/_types.py +1 -1
  5. sqlspec/adapters/adbc/adk/__init__.py +5 -0
  6. sqlspec/adapters/adbc/adk/store.py +870 -0
  7. sqlspec/adapters/adbc/config.py +62 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +52 -2
  9. sqlspec/adapters/adbc/driver.py +144 -45
  10. sqlspec/adapters/adbc/litestar/__init__.py +5 -0
  11. sqlspec/adapters/adbc/litestar/store.py +504 -0
  12. sqlspec/adapters/adbc/type_converter.py +44 -50
  13. sqlspec/adapters/aiosqlite/_types.py +1 -1
  14. sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
  15. sqlspec/adapters/aiosqlite/adk/store.py +527 -0
  16. sqlspec/adapters/aiosqlite/config.py +86 -16
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +34 -2
  18. sqlspec/adapters/aiosqlite/driver.py +127 -38
  19. sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
  20. sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
  21. sqlspec/adapters/aiosqlite/pool.py +7 -7
  22. sqlspec/adapters/asyncmy/__init__.py +7 -1
  23. sqlspec/adapters/asyncmy/_types.py +1 -1
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +59 -17
  27. sqlspec/adapters/asyncmy/data_dictionary.py +41 -2
  28. sqlspec/adapters/asyncmy/driver.py +293 -62
  29. sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
  30. sqlspec/adapters/asyncmy/litestar/store.py +296 -0
  31. sqlspec/adapters/asyncpg/__init__.py +2 -1
  32. sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
  33. sqlspec/adapters/asyncpg/_types.py +11 -7
  34. sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
  35. sqlspec/adapters/asyncpg/adk/store.py +450 -0
  36. sqlspec/adapters/asyncpg/config.py +57 -36
  37. sqlspec/adapters/asyncpg/data_dictionary.py +41 -2
  38. sqlspec/adapters/asyncpg/driver.py +153 -23
  39. sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
  40. sqlspec/adapters/asyncpg/litestar/store.py +253 -0
  41. sqlspec/adapters/bigquery/_types.py +1 -1
  42. sqlspec/adapters/bigquery/adk/__init__.py +5 -0
  43. sqlspec/adapters/bigquery/adk/store.py +576 -0
  44. sqlspec/adapters/bigquery/config.py +25 -11
  45. sqlspec/adapters/bigquery/data_dictionary.py +42 -2
  46. sqlspec/adapters/bigquery/driver.py +352 -144
  47. sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
  48. sqlspec/adapters/bigquery/litestar/store.py +327 -0
  49. sqlspec/adapters/bigquery/type_converter.py +55 -23
  50. sqlspec/adapters/duckdb/_types.py +2 -2
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +79 -21
  54. sqlspec/adapters/duckdb/data_dictionary.py +41 -2
  55. sqlspec/adapters/duckdb/driver.py +138 -43
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +5 -5
  59. sqlspec/adapters/duckdb/type_converter.py +51 -21
  60. sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
  61. sqlspec/adapters/oracledb/_types.py +20 -2
  62. sqlspec/adapters/oracledb/adk/__init__.py +5 -0
  63. sqlspec/adapters/oracledb/adk/store.py +1745 -0
  64. sqlspec/adapters/oracledb/config.py +120 -36
  65. sqlspec/adapters/oracledb/data_dictionary.py +87 -20
  66. sqlspec/adapters/oracledb/driver.py +292 -84
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +316 -25
  70. sqlspec/adapters/oracledb/type_converter.py +91 -16
  71. sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
  72. sqlspec/adapters/psqlpy/_types.py +2 -1
  73. sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
  74. sqlspec/adapters/psqlpy/adk/store.py +482 -0
  75. sqlspec/adapters/psqlpy/config.py +45 -19
  76. sqlspec/adapters/psqlpy/data_dictionary.py +41 -2
  77. sqlspec/adapters/psqlpy/driver.py +101 -31
  78. sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
  79. sqlspec/adapters/psqlpy/litestar/store.py +272 -0
  80. sqlspec/adapters/psqlpy/type_converter.py +40 -11
  81. sqlspec/adapters/psycopg/_type_handlers.py +80 -0
  82. sqlspec/adapters/psycopg/_types.py +2 -1
  83. sqlspec/adapters/psycopg/adk/__init__.py +5 -0
  84. sqlspec/adapters/psycopg/adk/store.py +944 -0
  85. sqlspec/adapters/psycopg/config.py +65 -37
  86. sqlspec/adapters/psycopg/data_dictionary.py +77 -3
  87. sqlspec/adapters/psycopg/driver.py +200 -78
  88. sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
  89. sqlspec/adapters/psycopg/litestar/store.py +554 -0
  90. sqlspec/adapters/sqlite/__init__.py +2 -1
  91. sqlspec/adapters/sqlite/_type_handlers.py +86 -0
  92. sqlspec/adapters/sqlite/_types.py +1 -1
  93. sqlspec/adapters/sqlite/adk/__init__.py +5 -0
  94. sqlspec/adapters/sqlite/adk/store.py +572 -0
  95. sqlspec/adapters/sqlite/config.py +85 -16
  96. sqlspec/adapters/sqlite/data_dictionary.py +34 -2
  97. sqlspec/adapters/sqlite/driver.py +120 -52
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +5 -5
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +91 -58
  104. sqlspec/builder/_column.py +5 -5
  105. sqlspec/builder/_ddl.py +98 -89
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +41 -44
  109. sqlspec/builder/_insert.py +5 -82
  110. sqlspec/builder/{mixins/_join_operations.py → _join.py} +145 -143
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +9 -11
  113. sqlspec/builder/_select.py +1313 -25
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +76 -69
  116. sqlspec/config.py +231 -60
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +18 -18
  119. sqlspec/core/compiler.py +6 -8
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +76 -45
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +32 -31
  126. sqlspec/core/type_conversion.py +3 -2
  127. sqlspec/driver/__init__.py +1 -3
  128. sqlspec/driver/_async.py +95 -161
  129. sqlspec/driver/_common.py +133 -80
  130. sqlspec/driver/_sync.py +95 -162
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +70 -7
  134. sqlspec/extensions/adk/__init__.py +53 -0
  135. sqlspec/extensions/adk/_types.py +51 -0
  136. sqlspec/extensions/adk/converters.py +172 -0
  137. sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
  138. sqlspec/extensions/adk/migrations/__init__.py +0 -0
  139. sqlspec/extensions/adk/service.py +181 -0
  140. sqlspec/extensions/adk/store.py +536 -0
  141. sqlspec/extensions/aiosql/adapter.py +73 -53
  142. sqlspec/extensions/litestar/__init__.py +21 -4
  143. sqlspec/extensions/litestar/cli.py +54 -10
  144. sqlspec/extensions/litestar/config.py +59 -266
  145. sqlspec/extensions/litestar/handlers.py +46 -17
  146. sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
  147. sqlspec/extensions/litestar/migrations/__init__.py +3 -0
  148. sqlspec/extensions/litestar/plugin.py +324 -223
  149. sqlspec/extensions/litestar/providers.py +25 -25
  150. sqlspec/extensions/litestar/store.py +265 -0
  151. sqlspec/loader.py +30 -49
  152. sqlspec/migrations/base.py +200 -76
  153. sqlspec/migrations/commands.py +591 -62
  154. sqlspec/migrations/context.py +6 -9
  155. sqlspec/migrations/fix.py +199 -0
  156. sqlspec/migrations/loaders.py +47 -19
  157. sqlspec/migrations/runner.py +241 -75
  158. sqlspec/migrations/tracker.py +237 -21
  159. sqlspec/migrations/utils.py +51 -3
  160. sqlspec/migrations/validation.py +177 -0
  161. sqlspec/protocols.py +66 -36
  162. sqlspec/storage/_utils.py +98 -0
  163. sqlspec/storage/backends/fsspec.py +134 -106
  164. sqlspec/storage/backends/local.py +78 -51
  165. sqlspec/storage/backends/obstore.py +278 -162
  166. sqlspec/storage/registry.py +75 -39
  167. sqlspec/typing.py +14 -84
  168. sqlspec/utils/config_resolver.py +6 -6
  169. sqlspec/utils/correlation.py +4 -5
  170. sqlspec/utils/data_transformation.py +3 -2
  171. sqlspec/utils/deprecation.py +9 -8
  172. sqlspec/utils/fixtures.py +4 -4
  173. sqlspec/utils/logging.py +46 -6
  174. sqlspec/utils/module_loader.py +2 -2
  175. sqlspec/utils/schema.py +288 -0
  176. sqlspec/utils/serializers.py +3 -3
  177. sqlspec/utils/sync_tools.py +21 -17
  178. sqlspec/utils/text.py +1 -2
  179. sqlspec/utils/type_guards.py +111 -20
  180. sqlspec/utils/version.py +433 -0
  181. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  182. sqlspec-0.27.0.dist-info/RECORD +207 -0
  183. sqlspec/builder/mixins/__init__.py +0 -55
  184. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -253
  185. sqlspec/builder/mixins/_delete_operations.py +0 -50
  186. sqlspec/builder/mixins/_insert_operations.py +0 -282
  187. sqlspec/builder/mixins/_merge_operations.py +0 -698
  188. sqlspec/builder/mixins/_order_limit_operations.py +0 -145
  189. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  190. sqlspec/builder/mixins/_select_operations.py +0 -930
  191. sqlspec/builder/mixins/_update_operations.py +0 -199
  192. sqlspec/builder/mixins/_where_clause.py +0 -1298
  193. sqlspec-0.26.0.dist-info/RECORD +0 -157
  194. sqlspec-0.26.0.dist-info/licenses/NOTICE +0 -29
  195. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  196. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  197. {sqlspec-0.26.0.dist-info → sqlspec-0.27.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  import logging
4
4
  from contextlib import asynccontextmanager
5
- from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union
5
+ from typing import TYPE_CHECKING, Any, ClassVar, TypedDict
6
6
 
7
7
  from typing_extensions import NotRequired
8
8
 
@@ -15,30 +15,31 @@ from sqlspec.adapters.aiosqlite.pool import (
15
15
  AiosqlitePoolConnection,
16
16
  )
17
17
  from sqlspec.config import AsyncDatabaseConfig
18
+ from sqlspec.utils.serializers import from_json, to_json
18
19
 
19
20
  if TYPE_CHECKING:
20
- from collections.abc import AsyncGenerator
21
+ from collections.abc import AsyncGenerator, Callable
21
22
 
22
23
  from sqlspec.core.statement import StatementConfig
23
24
 
24
- __all__ = ("AiosqliteConfig", "AiosqliteConnectionParams", "AiosqlitePoolParams")
25
+ __all__ = ("AiosqliteConfig", "AiosqliteConnectionParams", "AiosqliteDriverFeatures", "AiosqlitePoolParams")
25
26
 
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
29
- class AiosqliteConnectionParams(TypedDict, total=False):
30
+ class AiosqliteConnectionParams(TypedDict):
30
31
  """TypedDict for aiosqlite connection parameters."""
31
32
 
32
33
  database: NotRequired[str]
33
34
  timeout: NotRequired[float]
34
35
  detect_types: NotRequired[int]
35
- isolation_level: NotRequired[Optional[str]]
36
+ isolation_level: NotRequired[str | None]
36
37
  check_same_thread: NotRequired[bool]
37
38
  cached_statements: NotRequired[int]
38
39
  uri: NotRequired[bool]
39
40
 
40
41
 
41
- class AiosqlitePoolParams(AiosqliteConnectionParams, total=False):
42
+ class AiosqlitePoolParams(AiosqliteConnectionParams):
42
43
  """TypedDict for aiosqlite pool parameters, inheriting connection parameters."""
43
44
 
44
45
  pool_size: NotRequired[int]
@@ -48,21 +49,42 @@ class AiosqlitePoolParams(AiosqliteConnectionParams, total=False):
48
49
  extra: NotRequired[dict[str, Any]]
49
50
 
50
51
 
52
+ class AiosqliteDriverFeatures(TypedDict):
53
+ """Aiosqlite driver feature configuration.
54
+
55
+ Controls optional type handling and serialization features for SQLite connections.
56
+
57
+ enable_custom_adapters: Enable custom type adapters for JSON/UUID/datetime conversion.
58
+ Defaults to True for enhanced Python type support.
59
+ Set to False only if you need pure SQLite behavior without type conversions.
60
+ json_serializer: Custom JSON serializer function.
61
+ Defaults to sqlspec.utils.serializers.to_json.
62
+ json_deserializer: Custom JSON deserializer function.
63
+ Defaults to sqlspec.utils.serializers.from_json.
64
+ """
65
+
66
+ enable_custom_adapters: NotRequired[bool]
67
+ json_serializer: "NotRequired[Callable[[Any], str]]"
68
+ json_deserializer: "NotRequired[Callable[[str], Any]]"
69
+
70
+
51
71
  class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnectionPool, AiosqliteDriver]):
52
72
  """Database configuration for AioSQLite engine."""
53
73
 
54
74
  driver_type: "ClassVar[type[AiosqliteDriver]]" = AiosqliteDriver
55
75
  connection_type: "ClassVar[type[AiosqliteConnection]]" = AiosqliteConnection
76
+ supports_transactional_ddl: "ClassVar[bool]" = True
56
77
 
57
78
  def __init__(
58
79
  self,
59
80
  *,
60
- pool_config: "Optional[Union[AiosqlitePoolParams, dict[str, Any]]]" = None,
61
- pool_instance: "Optional[AiosqliteConnectionPool]" = None,
62
- migration_config: "Optional[dict[str, Any]]" = None,
63
- statement_config: "Optional[StatementConfig]" = None,
64
- driver_features: "Optional[dict[str, Any]]" = None,
65
- bind_key: "Optional[str]" = None,
81
+ pool_config: "AiosqlitePoolParams | dict[str, Any] | None" = None,
82
+ pool_instance: "AiosqliteConnectionPool | None" = None,
83
+ migration_config: "dict[str, Any] | None" = None,
84
+ statement_config: "StatementConfig | None" = None,
85
+ driver_features: "AiosqliteDriverFeatures | dict[str, Any] | None" = None,
86
+ bind_key: "str | None" = None,
87
+ extension_config: "dict[str, dict[str, Any]] | None" = None,
66
88
  ) -> None:
67
89
  """Initialize AioSQLite configuration.
68
90
 
@@ -73,20 +95,42 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
73
95
  statement_config: Optional statement configuration.
74
96
  driver_features: Optional driver feature configuration.
75
97
  bind_key: Optional unique identifier for this configuration.
98
+ extension_config: Extension-specific configuration (e.g., Litestar plugin settings)
76
99
  """
77
100
  config_dict = dict(pool_config) if pool_config else {}
78
101
 
79
102
  if "database" not in config_dict or config_dict["database"] == ":memory:":
80
103
  config_dict["database"] = "file::memory:?cache=shared"
81
104
  config_dict["uri"] = True
105
+ elif "database" in config_dict:
106
+ database_path = str(config_dict["database"])
107
+ if database_path.startswith("file:") and not config_dict.get("uri"):
108
+ logger.debug(
109
+ "Database URI detected (%s) but uri=True not set. "
110
+ "Auto-enabling URI mode to prevent physical file creation.",
111
+ database_path,
112
+ )
113
+ config_dict["uri"] = True
114
+
115
+ processed_driver_features: dict[str, Any] = dict(driver_features) if driver_features else {}
116
+
117
+ if "enable_custom_adapters" not in processed_driver_features:
118
+ processed_driver_features["enable_custom_adapters"] = True
119
+
120
+ if "json_serializer" not in processed_driver_features:
121
+ processed_driver_features["json_serializer"] = to_json
122
+
123
+ if "json_deserializer" not in processed_driver_features:
124
+ processed_driver_features["json_deserializer"] = from_json
82
125
 
83
126
  super().__init__(
84
127
  pool_config=config_dict,
85
128
  pool_instance=pool_instance,
86
129
  migration_config=migration_config,
87
130
  statement_config=statement_config or aiosqlite_statement_config,
88
- driver_features=driver_features or {},
131
+ driver_features=processed_driver_features,
89
132
  bind_key=bind_key,
133
+ extension_config=extension_config,
90
134
  )
91
135
 
92
136
  def _get_pool_config_dict(self) -> "dict[str, Any]":
@@ -138,7 +182,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
138
182
 
139
183
  @asynccontextmanager
140
184
  async def provide_session(
141
- self, *_args: Any, statement_config: "Optional[StatementConfig]" = None, **_kwargs: Any
185
+ self, *_args: Any, statement_config: "StatementConfig | None" = None, **_kwargs: Any
142
186
  ) -> "AsyncGenerator[AiosqliteDriver, None]":
143
187
  """Provide an async driver session context manager.
144
188
 
@@ -151,7 +195,11 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
151
195
  An AiosqliteDriver instance.
152
196
  """
153
197
  async with self.provide_connection(*_args, **_kwargs) as connection:
154
- yield self.driver_type(connection=connection, statement_config=statement_config or self.statement_config)
198
+ yield self.driver_type(
199
+ connection=connection,
200
+ statement_config=statement_config or self.statement_config,
201
+ driver_features=self.driver_features,
202
+ )
155
203
 
156
204
  async def _create_pool(self) -> AiosqliteConnectionPool:
157
205
  """Create the connection pool instance.
@@ -165,7 +213,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
165
213
  idle_timeout = config.pop("idle_timeout", 24 * 60 * 60)
166
214
  operation_timeout = config.pop("operation_timeout", 10.0)
167
215
 
168
- return AiosqliteConnectionPool(
216
+ pool = AiosqliteConnectionPool(
169
217
  connection_parameters=self._get_connection_config_dict(),
170
218
  pool_size=pool_size,
171
219
  connect_timeout=connect_timeout,
@@ -173,6 +221,28 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
173
221
  operation_timeout=operation_timeout,
174
222
  )
175
223
 
224
+ if self.driver_features.get("enable_custom_adapters", False):
225
+ self._register_type_adapters()
226
+
227
+ return pool
228
+
229
+ def _register_type_adapters(self) -> None:
230
+ """Register custom type adapters and converters for SQLite.
231
+
232
+ Called once during pool creation if enable_custom_adapters is True.
233
+ Registers JSON serialization handlers if configured.
234
+
235
+ Note: aiosqlite uses the same sqlite3 module type registration as the
236
+ sync adapter, so this shares the implementation.
237
+ """
238
+ if self.driver_features.get("enable_custom_adapters", False):
239
+ from sqlspec.adapters.sqlite._type_handlers import register_type_handlers
240
+
241
+ register_type_handlers(
242
+ json_serializer=self.driver_features.get("json_serializer"),
243
+ json_deserializer=self.driver_features.get("json_deserializer"),
244
+ )
245
+
176
246
  async def close_pool(self) -> None:
177
247
  """Close the connection pool."""
178
248
  if self.pool_instance and not self.pool_instance.is_closed:
@@ -1,12 +1,14 @@
1
1
  """SQLite-specific data dictionary for metadata queries via aiosqlite."""
2
2
 
3
3
  import re
4
- from typing import TYPE_CHECKING, Callable, Optional, cast
4
+ from typing import TYPE_CHECKING, Any, cast
5
5
 
6
6
  from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
7
7
  from sqlspec.utils.logging import get_logger
8
8
 
9
9
  if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
10
12
  from sqlspec.adapters.aiosqlite.driver import AiosqliteDriver
11
13
 
12
14
  logger = get_logger("adapters.aiosqlite.data_dictionary")
@@ -20,7 +22,7 @@ __all__ = ("AiosqliteAsyncDataDictionary",)
20
22
  class AiosqliteAsyncDataDictionary(AsyncDataDictionaryBase):
21
23
  """SQLite-specific async data dictionary via aiosqlite."""
22
24
 
23
- async def get_version(self, driver: AsyncDriverAdapterBase) -> "Optional[VersionInfo]":
25
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "VersionInfo | None":
24
26
  """Get SQLite database version information.
25
27
 
26
28
  Args:
@@ -97,6 +99,36 @@ class AiosqliteAsyncDataDictionary(AsyncDataDictionaryBase):
97
99
  type_map = {"uuid": "TEXT", "boolean": "INTEGER", "timestamp": "TIMESTAMP", "text": "TEXT", "blob": "BLOB"}
98
100
  return type_map.get(type_category, "TEXT")
99
101
 
102
+ async def get_columns(
103
+ self, driver: AsyncDriverAdapterBase, table: str, schema: "str | None" = None
104
+ ) -> "list[dict[str, Any]]":
105
+ """Get column information for a table using SQLite PRAGMA.
106
+
107
+ Args:
108
+ driver: AioSQLite driver instance
109
+ table: Table name to query columns for
110
+ schema: Schema name (unused in SQLite)
111
+
112
+ Returns:
113
+ List of column metadata dictionaries with keys:
114
+ - column_name: Name of the column
115
+ - data_type: SQLite data type
116
+ - nullable: Whether column allows NULL
117
+ - default_value: Default value if any
118
+ """
119
+ aiosqlite_driver = cast("AiosqliteDriver", driver)
120
+ result = await aiosqlite_driver.execute(f"PRAGMA table_info({table})")
121
+
122
+ return [
123
+ {
124
+ "column_name": row["name"] if isinstance(row, dict) else row[1],
125
+ "data_type": row["type"] if isinstance(row, dict) else row[2],
126
+ "nullable": not (row["notnull"] if isinstance(row, dict) else row[3]),
127
+ "default_value": row["dflt_value"] if isinstance(row, dict) else row[4],
128
+ }
129
+ for row in result.data or []
130
+ ]
131
+
100
132
  def list_available_features(self) -> "list[str]":
101
133
  """List available SQLite feature flags.
102
134
 
@@ -4,7 +4,7 @@ import asyncio
4
4
  import contextlib
5
5
  import datetime
6
6
  from decimal import Decimal
7
- from typing import TYPE_CHECKING, Any, Optional
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  import aiosqlite
10
10
 
@@ -12,7 +12,18 @@ from sqlspec.core.cache import get_cache_config
12
12
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
13
13
  from sqlspec.core.statement import StatementConfig
14
14
  from sqlspec.driver import AsyncDriverAdapterBase
15
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
15
+ from sqlspec.exceptions import (
16
+ CheckViolationError,
17
+ DatabaseConnectionError,
18
+ DataError,
19
+ ForeignKeyViolationError,
20
+ IntegrityError,
21
+ NotNullViolationError,
22
+ OperationalError,
23
+ SQLParsingError,
24
+ SQLSpecError,
25
+ UniqueViolationError,
26
+ )
16
27
  from sqlspec.utils.serializers import to_json
17
28
 
18
29
  if TYPE_CHECKING:
@@ -26,6 +37,15 @@ if TYPE_CHECKING:
26
37
 
27
38
  __all__ = ("AiosqliteCursor", "AiosqliteDriver", "AiosqliteExceptionHandler", "aiosqlite_statement_config")
28
39
 
40
+ SQLITE_CONSTRAINT_UNIQUE_CODE = 2067
41
+ SQLITE_CONSTRAINT_FOREIGNKEY_CODE = 787
42
+ SQLITE_CONSTRAINT_NOTNULL_CODE = 1811
43
+ SQLITE_CONSTRAINT_CHECK_CODE = 531
44
+ SQLITE_CONSTRAINT_CODE = 19
45
+ SQLITE_CANTOPEN_CODE = 14
46
+ SQLITE_IOERR_CODE = 10
47
+ SQLITE_MISMATCH_CODE = 20
48
+
29
49
 
30
50
  aiosqlite_statement_config = StatementConfig(
31
51
  dialect="sqlite",
@@ -61,7 +81,7 @@ class AiosqliteCursor:
61
81
 
62
82
  def __init__(self, connection: "AiosqliteConnection") -> None:
63
83
  self.connection = connection
64
- self.cursor: Optional[aiosqlite.Cursor] = None
84
+ self.cursor: aiosqlite.Cursor | None = None
65
85
 
66
86
  async def __aenter__(self) -> "aiosqlite.Cursor":
67
87
  self.cursor = await self.connection.cursor()
@@ -74,7 +94,11 @@ class AiosqliteCursor:
74
94
 
75
95
 
76
96
  class AiosqliteExceptionHandler:
77
- """Async context manager for AIOSQLite database exceptions."""
97
+ """Async context manager for handling aiosqlite database exceptions.
98
+
99
+ Maps SQLite extended result codes to specific SQLSpec exceptions
100
+ for better error handling in application code.
101
+ """
78
102
 
79
103
  __slots__ = ()
80
104
 
@@ -84,38 +108,103 @@ class AiosqliteExceptionHandler:
84
108
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
85
109
  if exc_type is None:
86
110
  return
87
- if issubclass(exc_type, aiosqlite.IntegrityError):
88
- e = exc_val
89
- msg = f"AIOSQLite integrity constraint violation: {e}"
90
- raise SQLSpecError(msg) from e
91
- if issubclass(exc_type, aiosqlite.OperationalError):
92
- e = exc_val
93
- error_msg = str(e).lower()
94
- if "locked" in error_msg:
95
- msg = f"AIOSQLite database locked: {e}. Consider enabling WAL mode or reducing concurrency."
96
- raise SQLSpecError(msg) from e
97
- if "syntax" in error_msg or "malformed" in error_msg:
98
- msg = f"AIOSQLite SQL syntax error: {e}"
99
- raise SQLParsingError(msg) from e
100
- msg = f"AIOSQLite operational error: {e}"
101
- raise SQLSpecError(msg) from e
102
- if issubclass(exc_type, aiosqlite.DatabaseError):
103
- e = exc_val
104
- msg = f"AIOSQLite database error: {e}"
105
- raise SQLSpecError(msg) from e
106
111
  if issubclass(exc_type, aiosqlite.Error):
107
- e = exc_val
108
- msg = f"AIOSQLite error: {e}"
109
- raise SQLSpecError(msg) from e
110
- if issubclass(exc_type, Exception):
111
- e = exc_val
112
- error_msg = str(e).lower()
113
- if "parse" in error_msg or "syntax" in error_msg:
114
- msg = f"SQL parsing failed: {e}"
115
- raise SQLParsingError(msg) from e
116
- msg = f"Unexpected async database operation error: {e}"
112
+ self._map_sqlite_exception(exc_val)
113
+
114
+ def _map_sqlite_exception(self, e: Any) -> None:
115
+ """Map SQLite exception to SQLSpec exception.
116
+
117
+ Args:
118
+ e: aiosqlite.Error instance
119
+
120
+ Raises:
121
+ Specific SQLSpec exception based on error code
122
+ """
123
+ error_code = getattr(e, "sqlite_errorcode", None)
124
+ error_name = getattr(e, "sqlite_errorname", None)
125
+ error_msg = str(e).lower()
126
+
127
+ if "locked" in error_msg:
128
+ msg = f"AIOSQLite database locked: {e}. Consider enabling WAL mode or reducing concurrency."
117
129
  raise SQLSpecError(msg) from e
118
130
 
131
+ if not error_code:
132
+ if "unique constraint" in error_msg:
133
+ self._raise_unique_violation(e, 0)
134
+ elif "foreign key constraint" in error_msg:
135
+ self._raise_foreign_key_violation(e, 0)
136
+ elif "not null constraint" in error_msg:
137
+ self._raise_not_null_violation(e, 0)
138
+ elif "check constraint" in error_msg:
139
+ self._raise_check_violation(e, 0)
140
+ elif "syntax" in error_msg:
141
+ self._raise_parsing_error(e, None)
142
+ else:
143
+ self._raise_generic_error(e)
144
+ return
145
+
146
+ if error_code == SQLITE_CONSTRAINT_UNIQUE_CODE or error_name == "SQLITE_CONSTRAINT_UNIQUE":
147
+ self._raise_unique_violation(e, error_code)
148
+ elif error_code == SQLITE_CONSTRAINT_FOREIGNKEY_CODE or error_name == "SQLITE_CONSTRAINT_FOREIGNKEY":
149
+ self._raise_foreign_key_violation(e, error_code)
150
+ elif error_code == SQLITE_CONSTRAINT_NOTNULL_CODE or error_name == "SQLITE_CONSTRAINT_NOTNULL":
151
+ self._raise_not_null_violation(e, error_code)
152
+ elif error_code == SQLITE_CONSTRAINT_CHECK_CODE or error_name == "SQLITE_CONSTRAINT_CHECK":
153
+ self._raise_check_violation(e, error_code)
154
+ elif error_code == SQLITE_CONSTRAINT_CODE or error_name == "SQLITE_CONSTRAINT":
155
+ self._raise_integrity_error(e, error_code)
156
+ elif error_code == SQLITE_CANTOPEN_CODE or error_name == "SQLITE_CANTOPEN":
157
+ self._raise_connection_error(e, error_code)
158
+ elif error_code == SQLITE_IOERR_CODE or error_name == "SQLITE_IOERR":
159
+ self._raise_operational_error(e, error_code)
160
+ elif error_code == SQLITE_MISMATCH_CODE or error_name == "SQLITE_MISMATCH":
161
+ self._raise_data_error(e, error_code)
162
+ elif error_code == 1 or "syntax" in error_msg:
163
+ self._raise_parsing_error(e, error_code)
164
+ else:
165
+ self._raise_generic_error(e)
166
+
167
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
168
+ msg = f"SQLite unique constraint violation [code {code}]: {e}"
169
+ raise UniqueViolationError(msg) from e
170
+
171
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
172
+ msg = f"SQLite foreign key constraint violation [code {code}]: {e}"
173
+ raise ForeignKeyViolationError(msg) from e
174
+
175
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
176
+ msg = f"SQLite not-null constraint violation [code {code}]: {e}"
177
+ raise NotNullViolationError(msg) from e
178
+
179
+ def _raise_check_violation(self, e: Any, code: int) -> None:
180
+ msg = f"SQLite check constraint violation [code {code}]: {e}"
181
+ raise CheckViolationError(msg) from e
182
+
183
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
184
+ msg = f"SQLite integrity constraint violation [code {code}]: {e}"
185
+ raise IntegrityError(msg) from e
186
+
187
+ def _raise_parsing_error(self, e: Any, code: "int | None") -> None:
188
+ code_str = f"[code {code}]" if code else ""
189
+ msg = f"SQLite SQL syntax error {code_str}: {e}"
190
+ raise SQLParsingError(msg) from e
191
+
192
+ def _raise_connection_error(self, e: Any, code: int) -> None:
193
+ msg = f"SQLite connection error [code {code}]: {e}"
194
+ raise DatabaseConnectionError(msg) from e
195
+
196
+ def _raise_operational_error(self, e: Any, code: int) -> None:
197
+ msg = f"SQLite operational error [code {code}]: {e}"
198
+ raise OperationalError(msg) from e
199
+
200
+ def _raise_data_error(self, e: Any, code: int) -> None:
201
+ msg = f"SQLite data error [code {code}]: {e}"
202
+ raise DataError(msg) from e
203
+
204
+ def _raise_generic_error(self, e: Any) -> None:
205
+ msg = f"SQLite database error: {e}"
206
+ raise SQLSpecError(msg) from e
207
+
119
208
 
120
209
  class AiosqliteDriver(AsyncDriverAdapterBase):
121
210
  """AIOSQLite driver for async SQLite database operations."""
@@ -126,15 +215,15 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
126
215
  def __init__(
127
216
  self,
128
217
  connection: "AiosqliteConnection",
129
- statement_config: "Optional[StatementConfig]" = None,
130
- driver_features: "Optional[dict[str, Any]]" = None,
218
+ statement_config: "StatementConfig | None" = None,
219
+ driver_features: "dict[str, Any] | None" = None,
131
220
  ) -> None:
132
221
  if statement_config is None:
133
222
  cache_config = get_cache_config()
134
223
  statement_config = aiosqlite_statement_config.replace(enable_caching=cache_config.compiled_cache_enabled)
135
224
 
136
225
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
137
- self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
226
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
138
227
 
139
228
  def with_cursor(self, connection: "AiosqliteConnection") -> "AiosqliteCursor":
140
229
  """Create async context manager for AIOSQLite cursor."""
@@ -144,7 +233,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
144
233
  """Handle AIOSQLite-specific exceptions."""
145
234
  return AiosqliteExceptionHandler()
146
235
 
147
- async def _try_special_handling(self, cursor: "aiosqlite.Cursor", statement: "SQL") -> "Optional[SQLResult]":
236
+ async def _try_special_handling(self, cursor: "aiosqlite.Cursor", statement: "SQL") -> "SQLResult | None":
148
237
  """Hook for AIOSQLite-specific special operations.
149
238
 
150
239
  Args:
@@ -196,7 +285,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
196
285
  fetched_data = await cursor.fetchall()
197
286
  column_names = [col[0] for col in cursor.description or []]
198
287
 
199
- data = [dict(zip(column_names, row)) for row in fetched_data]
288
+ data = [dict(zip(column_names, row, strict=False)) for row in fetched_data]
200
289
 
201
290
  return self.create_execution_result(
202
291
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -0,0 +1,5 @@
1
+ """Litestar integration for AioSQLite adapter."""
2
+
3
+ from sqlspec.adapters.aiosqlite.litestar.store import AiosqliteStore
4
+
5
+ __all__ = ("AiosqliteStore",)