sqlspec 0.25.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 (199) hide show
  1. sqlspec/__init__.py +7 -15
  2. sqlspec/_serialization.py +256 -24
  3. sqlspec/_typing.py +71 -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 +69 -12
  8. sqlspec/adapters/adbc/data_dictionary.py +340 -0
  9. sqlspec/adapters/adbc/driver.py +266 -58
  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 +153 -0
  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 +88 -15
  17. sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
  18. sqlspec/adapters/aiosqlite/driver.py +143 -40
  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 +2 -2
  24. sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
  25. sqlspec/adapters/asyncmy/adk/store.py +493 -0
  26. sqlspec/adapters/asyncmy/config.py +68 -23
  27. sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
  28. sqlspec/adapters/asyncmy/driver.py +313 -58
  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 +59 -35
  37. sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
  38. sqlspec/adapters/asyncpg/driver.py +170 -25
  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 +27 -10
  45. sqlspec/adapters/bigquery/data_dictionary.py +149 -0
  46. sqlspec/adapters/bigquery/driver.py +368 -142
  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 +125 -0
  50. sqlspec/adapters/duckdb/_types.py +1 -1
  51. sqlspec/adapters/duckdb/adk/__init__.py +14 -0
  52. sqlspec/adapters/duckdb/adk/store.py +553 -0
  53. sqlspec/adapters/duckdb/config.py +80 -20
  54. sqlspec/adapters/duckdb/data_dictionary.py +163 -0
  55. sqlspec/adapters/duckdb/driver.py +167 -45
  56. sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
  57. sqlspec/adapters/duckdb/litestar/store.py +332 -0
  58. sqlspec/adapters/duckdb/pool.py +4 -4
  59. sqlspec/adapters/duckdb/type_converter.py +133 -0
  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 +122 -32
  65. sqlspec/adapters/oracledb/data_dictionary.py +509 -0
  66. sqlspec/adapters/oracledb/driver.py +353 -91
  67. sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
  68. sqlspec/adapters/oracledb/litestar/store.py +767 -0
  69. sqlspec/adapters/oracledb/migrations.py +348 -73
  70. sqlspec/adapters/oracledb/type_converter.py +207 -0
  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 +46 -17
  76. sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
  77. sqlspec/adapters/psqlpy/driver.py +123 -209
  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 +102 -0
  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 +69 -35
  86. sqlspec/adapters/psycopg/data_dictionary.py +331 -0
  87. sqlspec/adapters/psycopg/driver.py +238 -81
  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 +87 -15
  96. sqlspec/adapters/sqlite/data_dictionary.py +149 -0
  97. sqlspec/adapters/sqlite/driver.py +137 -54
  98. sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
  99. sqlspec/adapters/sqlite/litestar/store.py +318 -0
  100. sqlspec/adapters/sqlite/pool.py +18 -9
  101. sqlspec/base.py +45 -26
  102. sqlspec/builder/__init__.py +73 -4
  103. sqlspec/builder/_base.py +162 -89
  104. sqlspec/builder/_column.py +62 -29
  105. sqlspec/builder/_ddl.py +180 -121
  106. sqlspec/builder/_delete.py +5 -4
  107. sqlspec/builder/_dml.py +388 -0
  108. sqlspec/{_sql.py → builder/_factory.py} +53 -94
  109. sqlspec/builder/_insert.py +32 -131
  110. sqlspec/builder/_join.py +375 -0
  111. sqlspec/builder/_merge.py +446 -11
  112. sqlspec/builder/_parsing_utils.py +111 -17
  113. sqlspec/builder/_select.py +1457 -24
  114. sqlspec/builder/_update.py +11 -42
  115. sqlspec/cli.py +307 -194
  116. sqlspec/config.py +252 -67
  117. sqlspec/core/__init__.py +5 -4
  118. sqlspec/core/cache.py +17 -17
  119. sqlspec/core/compiler.py +62 -9
  120. sqlspec/core/filters.py +37 -37
  121. sqlspec/core/hashing.py +9 -9
  122. sqlspec/core/parameters.py +83 -48
  123. sqlspec/core/result.py +102 -46
  124. sqlspec/core/splitter.py +16 -17
  125. sqlspec/core/statement.py +36 -30
  126. sqlspec/core/type_conversion.py +235 -0
  127. sqlspec/driver/__init__.py +7 -6
  128. sqlspec/driver/_async.py +188 -151
  129. sqlspec/driver/_common.py +285 -80
  130. sqlspec/driver/_sync.py +188 -152
  131. sqlspec/driver/mixins/_result_tools.py +20 -236
  132. sqlspec/driver/mixins/_sql_translator.py +4 -4
  133. sqlspec/exceptions.py +75 -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/__init__.py +4 -3
  153. sqlspec/migrations/base.py +302 -39
  154. sqlspec/migrations/commands.py +611 -144
  155. sqlspec/migrations/context.py +142 -0
  156. sqlspec/migrations/fix.py +199 -0
  157. sqlspec/migrations/loaders.py +68 -23
  158. sqlspec/migrations/runner.py +543 -107
  159. sqlspec/migrations/tracker.py +237 -21
  160. sqlspec/migrations/utils.py +51 -3
  161. sqlspec/migrations/validation.py +177 -0
  162. sqlspec/protocols.py +66 -36
  163. sqlspec/storage/_utils.py +98 -0
  164. sqlspec/storage/backends/fsspec.py +134 -106
  165. sqlspec/storage/backends/local.py +78 -51
  166. sqlspec/storage/backends/obstore.py +278 -162
  167. sqlspec/storage/registry.py +75 -39
  168. sqlspec/typing.py +16 -84
  169. sqlspec/utils/config_resolver.py +153 -0
  170. sqlspec/utils/correlation.py +4 -5
  171. sqlspec/utils/data_transformation.py +3 -2
  172. sqlspec/utils/deprecation.py +9 -8
  173. sqlspec/utils/fixtures.py +4 -4
  174. sqlspec/utils/logging.py +46 -6
  175. sqlspec/utils/module_loader.py +2 -2
  176. sqlspec/utils/schema.py +288 -0
  177. sqlspec/utils/serializers.py +50 -2
  178. sqlspec/utils/sync_tools.py +21 -17
  179. sqlspec/utils/text.py +1 -2
  180. sqlspec/utils/type_guards.py +111 -20
  181. sqlspec/utils/version.py +433 -0
  182. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
  183. sqlspec-0.27.0.dist-info/RECORD +207 -0
  184. sqlspec/builder/mixins/__init__.py +0 -55
  185. sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
  186. sqlspec/builder/mixins/_delete_operations.py +0 -50
  187. sqlspec/builder/mixins/_insert_operations.py +0 -282
  188. sqlspec/builder/mixins/_join_operations.py +0 -389
  189. sqlspec/builder/mixins/_merge_operations.py +0 -592
  190. sqlspec/builder/mixins/_order_limit_operations.py +0 -152
  191. sqlspec/builder/mixins/_pivot_operations.py +0 -157
  192. sqlspec/builder/mixins/_select_operations.py +0 -936
  193. sqlspec/builder/mixins/_update_operations.py +0 -218
  194. sqlspec/builder/mixins/_where_clause.py +0 -1304
  195. sqlspec-0.25.0.dist-info/RECORD +0 -139
  196. sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
  197. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
  198. {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
  199. {sqlspec-0.25.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,20 +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,
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,
65
88
  ) -> None:
66
89
  """Initialize AioSQLite configuration.
67
90
 
@@ -71,19 +94,43 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
71
94
  migration_config: Optional migration configuration.
72
95
  statement_config: Optional statement configuration.
73
96
  driver_features: Optional driver feature configuration.
97
+ bind_key: Optional unique identifier for this configuration.
98
+ extension_config: Extension-specific configuration (e.g., Litestar plugin settings)
74
99
  """
75
100
  config_dict = dict(pool_config) if pool_config else {}
76
101
 
77
102
  if "database" not in config_dict or config_dict["database"] == ":memory:":
78
103
  config_dict["database"] = "file::memory:?cache=shared"
79
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
80
125
 
81
126
  super().__init__(
82
127
  pool_config=config_dict,
83
128
  pool_instance=pool_instance,
84
129
  migration_config=migration_config,
85
130
  statement_config=statement_config or aiosqlite_statement_config,
86
- driver_features=driver_features or {},
131
+ driver_features=processed_driver_features,
132
+ bind_key=bind_key,
133
+ extension_config=extension_config,
87
134
  )
88
135
 
89
136
  def _get_pool_config_dict(self) -> "dict[str, Any]":
@@ -135,7 +182,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
135
182
 
136
183
  @asynccontextmanager
137
184
  async def provide_session(
138
- self, *_args: Any, statement_config: "Optional[StatementConfig]" = None, **_kwargs: Any
185
+ self, *_args: Any, statement_config: "StatementConfig | None" = None, **_kwargs: Any
139
186
  ) -> "AsyncGenerator[AiosqliteDriver, None]":
140
187
  """Provide an async driver session context manager.
141
188
 
@@ -148,7 +195,11 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
148
195
  An AiosqliteDriver instance.
149
196
  """
150
197
  async with self.provide_connection(*_args, **_kwargs) as connection:
151
- 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
+ )
152
203
 
153
204
  async def _create_pool(self) -> AiosqliteConnectionPool:
154
205
  """Create the connection pool instance.
@@ -162,7 +213,7 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
162
213
  idle_timeout = config.pop("idle_timeout", 24 * 60 * 60)
163
214
  operation_timeout = config.pop("operation_timeout", 10.0)
164
215
 
165
- return AiosqliteConnectionPool(
216
+ pool = AiosqliteConnectionPool(
166
217
  connection_parameters=self._get_connection_config_dict(),
167
218
  pool_size=pool_size,
168
219
  connect_timeout=connect_timeout,
@@ -170,6 +221,28 @@ class AiosqliteConfig(AsyncDatabaseConfig["AiosqliteConnection", AiosqliteConnec
170
221
  operation_timeout=operation_timeout,
171
222
  )
172
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
+
173
246
  async def close_pool(self) -> None:
174
247
  """Close the connection pool."""
175
248
  if self.pool_instance and not self.pool_instance.is_closed:
@@ -0,0 +1,149 @@
1
+ """SQLite-specific data dictionary for metadata queries via aiosqlite."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ from sqlspec.driver import AsyncDataDictionaryBase, AsyncDriverAdapterBase, VersionInfo
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Callable
11
+
12
+ from sqlspec.adapters.aiosqlite.driver import AiosqliteDriver
13
+
14
+ logger = get_logger("adapters.aiosqlite.data_dictionary")
15
+
16
+ # Compiled regex patterns
17
+ SQLITE_VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)")
18
+
19
+ __all__ = ("AiosqliteAsyncDataDictionary",)
20
+
21
+
22
+ class AiosqliteAsyncDataDictionary(AsyncDataDictionaryBase):
23
+ """SQLite-specific async data dictionary via aiosqlite."""
24
+
25
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "VersionInfo | None":
26
+ """Get SQLite database version information.
27
+
28
+ Args:
29
+ driver: Async database driver instance
30
+
31
+ Returns:
32
+ SQLite version information or None if detection fails
33
+ """
34
+ version_str = await cast("AiosqliteDriver", driver).select_value("SELECT sqlite_version()")
35
+ if not version_str:
36
+ logger.warning("No SQLite version information found")
37
+ return None
38
+
39
+ # Parse version like "3.45.0"
40
+ version_match = SQLITE_VERSION_PATTERN.match(str(version_str))
41
+ if not version_match:
42
+ logger.warning("Could not parse SQLite version: %s", version_str)
43
+ return None
44
+
45
+ major, minor, patch = map(int, version_match.groups())
46
+ version_info = VersionInfo(major, minor, patch)
47
+ logger.debug("Detected SQLite version: %s", version_info)
48
+ return version_info
49
+
50
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
51
+ """Check if SQLite database supports a specific feature.
52
+
53
+ Args:
54
+ driver: AIOSQLite driver instance
55
+ feature: Feature name to check
56
+
57
+ Returns:
58
+ True if feature is supported, False otherwise
59
+ """
60
+ version_info = await self.get_version(driver)
61
+ if not version_info:
62
+ return False
63
+
64
+ feature_checks: dict[str, Callable[..., bool]] = {
65
+ "supports_json": lambda v: v >= VersionInfo(3, 38, 0),
66
+ "supports_returning": lambda v: v >= VersionInfo(3, 35, 0),
67
+ "supports_upsert": lambda v: v >= VersionInfo(3, 24, 0),
68
+ "supports_window_functions": lambda v: v >= VersionInfo(3, 25, 0),
69
+ "supports_cte": lambda v: v >= VersionInfo(3, 8, 3),
70
+ "supports_transactions": lambda _: True,
71
+ "supports_prepared_statements": lambda _: True,
72
+ "supports_schemas": lambda _: False, # SQLite has ATTACH but not schemas
73
+ "supports_arrays": lambda _: False,
74
+ "supports_uuid": lambda _: False,
75
+ }
76
+
77
+ if feature in feature_checks:
78
+ return bool(feature_checks[feature](version_info))
79
+
80
+ return False
81
+
82
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
83
+ """Get optimal SQLite type for a category.
84
+
85
+ Args:
86
+ driver: AIOSQLite driver instance
87
+ type_category: Type category
88
+
89
+ Returns:
90
+ SQLite-specific type name
91
+ """
92
+ version_info = await self.get_version(driver)
93
+
94
+ if type_category == "json":
95
+ if version_info and version_info >= VersionInfo(3, 38, 0):
96
+ return "JSON"
97
+ return "TEXT"
98
+
99
+ type_map = {"uuid": "TEXT", "boolean": "INTEGER", "timestamp": "TIMESTAMP", "text": "TEXT", "blob": "BLOB"}
100
+ return type_map.get(type_category, "TEXT")
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
+
132
+ def list_available_features(self) -> "list[str]":
133
+ """List available SQLite feature flags.
134
+
135
+ Returns:
136
+ List of supported feature names
137
+ """
138
+ return [
139
+ "supports_json",
140
+ "supports_returning",
141
+ "supports_upsert",
142
+ "supports_window_functions",
143
+ "supports_cte",
144
+ "supports_transactions",
145
+ "supports_prepared_statements",
146
+ "supports_schemas",
147
+ "supports_arrays",
148
+ "supports_uuid",
149
+ ]
@@ -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:
@@ -22,9 +33,19 @@ if TYPE_CHECKING:
22
33
  from sqlspec.core.result import SQLResult
23
34
  from sqlspec.core.statement import SQL
24
35
  from sqlspec.driver import ExecutionResult
36
+ from sqlspec.driver._async import AsyncDataDictionaryBase
25
37
 
26
38
  __all__ = ("AiosqliteCursor", "AiosqliteDriver", "AiosqliteExceptionHandler", "aiosqlite_statement_config")
27
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
+
28
49
 
29
50
  aiosqlite_statement_config = StatementConfig(
30
51
  dialect="sqlite",
@@ -60,21 +81,24 @@ class AiosqliteCursor:
60
81
 
61
82
  def __init__(self, connection: "AiosqliteConnection") -> None:
62
83
  self.connection = connection
63
- self.cursor: Optional[aiosqlite.Cursor] = None
84
+ self.cursor: aiosqlite.Cursor | None = None
64
85
 
65
86
  async def __aenter__(self) -> "aiosqlite.Cursor":
66
87
  self.cursor = await self.connection.cursor()
67
88
  return self.cursor
68
89
 
69
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
70
- _ = (exc_type, exc_val, exc_tb)
90
+ async def __aexit__(self, *_: Any) -> None:
71
91
  if self.cursor is not None:
72
92
  with contextlib.suppress(Exception):
73
93
  await self.cursor.close()
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,56 +108,122 @@ 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."""
122
211
 
123
- __slots__ = ()
212
+ __slots__ = ("_data_dictionary",)
124
213
  dialect = "sqlite"
125
214
 
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)
226
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
137
227
 
138
228
  def with_cursor(self, connection: "AiosqliteConnection") -> "AiosqliteCursor":
139
229
  """Create async context manager for AIOSQLite cursor."""
@@ -143,7 +233,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
143
233
  """Handle AIOSQLite-specific exceptions."""
144
234
  return AiosqliteExceptionHandler()
145
235
 
146
- 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":
147
237
  """Hook for AIOSQLite-specific special operations.
148
238
 
149
239
  Args:
@@ -195,7 +285,7 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
195
285
  fetched_data = await cursor.fetchall()
196
286
  column_names = [col[0] for col in cursor.description or []]
197
287
 
198
- 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]
199
289
 
200
290
  return self.create_execution_result(
201
291
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -241,3 +331,16 @@ class AiosqliteDriver(AsyncDriverAdapterBase):
241
331
  except aiosqlite.Error as e:
242
332
  msg = f"Failed to commit transaction: {e}"
243
333
  raise SQLSpecError(msg) from e
334
+
335
+ @property
336
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
337
+ """Get the data dictionary for this driver.
338
+
339
+ Returns:
340
+ Data dictionary instance for metadata queries
341
+ """
342
+ if self._data_dictionary is None:
343
+ from sqlspec.adapters.aiosqlite.data_dictionary import AiosqliteAsyncDataDictionary
344
+
345
+ self._data_dictionary = AiosqliteAsyncDataDictionary()
346
+ return self._data_dictionary
@@ -0,0 +1,5 @@
1
+ """Litestar integration for AioSQLite adapter."""
2
+
3
+ from sqlspec.adapters.aiosqlite.litestar.store import AiosqliteStore
4
+
5
+ __all__ = ("AiosqliteStore",)