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
@@ -1,36 +1,60 @@
1
1
  """SQLite database configuration with thread-local connections."""
2
2
 
3
+ import logging
3
4
  import uuid
4
5
  from contextlib import contextmanager
5
- from typing import TYPE_CHECKING, Any, ClassVar, Optional, TypedDict, Union, cast
6
+ from typing import TYPE_CHECKING, Any, ClassVar, TypedDict, cast
6
7
 
7
8
  from typing_extensions import NotRequired
8
9
 
10
+ from sqlspec.adapters.sqlite._type_handlers import register_type_handlers
9
11
  from sqlspec.adapters.sqlite._types import SqliteConnection
10
12
  from sqlspec.adapters.sqlite.driver import SqliteCursor, SqliteDriver, sqlite_statement_config
11
13
  from sqlspec.adapters.sqlite.pool import SqliteConnectionPool
12
14
  from sqlspec.config import SyncDatabaseConfig
15
+ from sqlspec.utils.serializers import from_json, to_json
16
+
17
+ logger = logging.getLogger(__name__)
13
18
 
14
19
  if TYPE_CHECKING:
15
- from collections.abc import Generator
20
+ from collections.abc import Callable, Generator
16
21
 
17
22
  from sqlspec.core.statement import StatementConfig
18
23
 
19
24
 
20
- class SqliteConnectionParams(TypedDict, total=False):
25
+ class SqliteConnectionParams(TypedDict):
21
26
  """SQLite connection parameters."""
22
27
 
23
28
  database: NotRequired[str]
24
29
  timeout: NotRequired[float]
25
30
  detect_types: NotRequired[int]
26
- isolation_level: "NotRequired[Optional[str]]"
31
+ isolation_level: "NotRequired[str | None]"
27
32
  check_same_thread: NotRequired[bool]
28
- factory: "NotRequired[Optional[type[SqliteConnection]]]"
33
+ factory: "NotRequired[type[SqliteConnection] | None]"
29
34
  cached_statements: NotRequired[int]
30
35
  uri: NotRequired[bool]
31
36
 
32
37
 
33
- __all__ = ("SqliteConfig", "SqliteConnectionParams")
38
+ class SqliteDriverFeatures(TypedDict):
39
+ """SQLite driver feature configuration.
40
+
41
+ Controls optional type handling and serialization features for SQLite connections.
42
+
43
+ enable_custom_adapters: Enable custom type adapters for JSON/UUID/datetime conversion.
44
+ Defaults to True for enhanced Python type support.
45
+ Set to False only if you need pure SQLite behavior without type conversions.
46
+ json_serializer: Custom JSON serializer function.
47
+ Defaults to sqlspec.utils.serializers.to_json.
48
+ json_deserializer: Custom JSON deserializer function.
49
+ Defaults to sqlspec.utils.serializers.from_json.
50
+ """
51
+
52
+ enable_custom_adapters: NotRequired[bool]
53
+ json_serializer: "NotRequired[Callable[[Any], str]]"
54
+ json_deserializer: "NotRequired[Callable[[str], Any]]"
55
+
56
+
57
+ __all__ = ("SqliteConfig", "SqliteConnectionParams", "SqliteDriverFeatures")
34
58
 
35
59
 
36
60
  class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, SqliteDriver]):
@@ -38,15 +62,18 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
38
62
 
39
63
  driver_type: "ClassVar[type[SqliteDriver]]" = SqliteDriver
40
64
  connection_type: "ClassVar[type[SqliteConnection]]" = SqliteConnection
65
+ supports_transactional_ddl: "ClassVar[bool]" = True
41
66
 
42
67
  def __init__(
43
68
  self,
44
69
  *,
45
- pool_config: "Optional[Union[SqliteConnectionParams, dict[str, Any]]]" = None,
46
- pool_instance: "Optional[SqliteConnectionPool]" = None,
47
- migration_config: "Optional[dict[str, Any]]" = None,
48
- statement_config: "Optional[StatementConfig]" = None,
49
- driver_features: "Optional[dict[str, Any]]" = None,
70
+ pool_config: "SqliteConnectionParams | dict[str, Any] | None" = None,
71
+ pool_instance: "SqliteConnectionPool | None" = None,
72
+ migration_config: "dict[str, Any] | None" = None,
73
+ statement_config: "StatementConfig | None" = None,
74
+ driver_features: "SqliteDriverFeatures | dict[str, Any] | None" = None,
75
+ bind_key: "str | None" = None,
76
+ extension_config: "dict[str, dict[str, Any]] | None" = None,
50
77
  ) -> None:
51
78
  """Initialize SQLite configuration.
52
79
 
@@ -56,19 +83,43 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
56
83
  migration_config: Migration configuration
57
84
  statement_config: Default SQL statement configuration
58
85
  driver_features: Optional driver feature configuration
86
+ bind_key: Optional bind key for the configuration
87
+ extension_config: Extension-specific configuration (e.g., Litestar plugin settings)
59
88
  """
60
89
  if pool_config is None:
61
90
  pool_config = {}
62
91
  if "database" not in pool_config or pool_config["database"] == ":memory:":
63
92
  pool_config["database"] = f"file:memory_{uuid.uuid4().hex}?mode=memory&cache=private"
64
93
  pool_config["uri"] = True
94
+ elif "database" in pool_config:
95
+ database_path = str(pool_config["database"])
96
+ if database_path.startswith("file:") and not pool_config.get("uri"):
97
+ logger.debug(
98
+ "Database URI detected (%s) but uri=True not set. "
99
+ "Auto-enabling URI mode to prevent physical file creation.",
100
+ database_path,
101
+ )
102
+ pool_config["uri"] = True
103
+
104
+ processed_driver_features: dict[str, Any] = dict(driver_features) if driver_features else {}
105
+
106
+ if "enable_custom_adapters" not in processed_driver_features:
107
+ processed_driver_features["enable_custom_adapters"] = True
108
+
109
+ if "json_serializer" not in processed_driver_features:
110
+ processed_driver_features["json_serializer"] = to_json
111
+
112
+ if "json_deserializer" not in processed_driver_features:
113
+ processed_driver_features["json_deserializer"] = from_json
65
114
 
66
115
  super().__init__(
116
+ bind_key=bind_key,
67
117
  pool_instance=pool_instance,
68
118
  pool_config=cast("dict[str, Any]", pool_config),
69
119
  migration_config=migration_config,
70
120
  statement_config=statement_config or sqlite_statement_config,
71
- driver_features=driver_features or {},
121
+ driver_features=processed_driver_features,
122
+ extension_config=extension_config,
72
123
  )
73
124
 
74
125
  def _get_connection_config_dict(self) -> "dict[str, Any]":
@@ -81,7 +132,24 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
81
132
  """Create connection pool from configuration."""
82
133
  config_dict = self._get_connection_config_dict()
83
134
 
84
- return SqliteConnectionPool(connection_parameters=config_dict, **self.pool_config)
135
+ pool = SqliteConnectionPool(connection_parameters=config_dict, **self.pool_config)
136
+
137
+ if self.driver_features.get("enable_custom_adapters", False):
138
+ self._register_type_adapters()
139
+
140
+ return pool
141
+
142
+ def _register_type_adapters(self) -> None:
143
+ """Register custom type adapters and converters for SQLite.
144
+
145
+ Called once during pool creation if enable_custom_adapters is True.
146
+ Registers JSON serialization handlers if configured.
147
+ """
148
+ if self.driver_features.get("enable_custom_adapters", False):
149
+ register_type_handlers(
150
+ json_serializer=self.driver_features.get("json_serializer"),
151
+ json_deserializer=self.driver_features.get("json_deserializer"),
152
+ )
85
153
 
86
154
  def _close_pool(self) -> None:
87
155
  """Close the connection pool."""
@@ -110,7 +178,7 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
110
178
 
111
179
  @contextmanager
112
180
  def provide_session(
113
- self, *args: "Any", statement_config: "Optional[StatementConfig]" = None, **kwargs: "Any"
181
+ self, *args: "Any", statement_config: "StatementConfig | None" = None, **kwargs: "Any"
114
182
  ) -> "Generator[SqliteDriver, None, None]":
115
183
  """Provide a SQLite driver session.
116
184
 
@@ -118,7 +186,11 @@ class SqliteConfig(SyncDatabaseConfig[SqliteConnection, SqliteConnectionPool, Sq
118
186
  SqliteDriver: A driver instance with thread-local connection
119
187
  """
120
188
  with self.provide_connection(*args, **kwargs) as connection:
121
- yield self.driver_type(connection=connection, statement_config=statement_config or self.statement_config)
189
+ yield self.driver_type(
190
+ connection=connection,
191
+ statement_config=statement_config or self.statement_config,
192
+ driver_features=self.driver_features,
193
+ )
122
194
 
123
195
  def get_signature_namespace(self) -> "dict[str, type[Any]]":
124
196
  """Get the signature namespace for SQLite types.
@@ -0,0 +1,149 @@
1
+ """SQLite-specific data dictionary for metadata queries."""
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Any, cast
5
+
6
+ from sqlspec.driver import SyncDataDictionaryBase, SyncDriverAdapterBase, 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.sqlite.driver import SqliteDriver
13
+
14
+ logger = get_logger("adapters.sqlite.data_dictionary")
15
+
16
+ # Compiled regex patterns
17
+ SQLITE_VERSION_PATTERN = re.compile(r"(\d+)\.(\d+)\.(\d+)")
18
+
19
+ __all__ = ("SqliteSyncDataDictionary",)
20
+
21
+
22
+ class SqliteSyncDataDictionary(SyncDataDictionaryBase):
23
+ """SQLite-specific sync data dictionary."""
24
+
25
+ def get_version(self, driver: SyncDriverAdapterBase) -> "VersionInfo | None":
26
+ """Get SQLite database version information.
27
+
28
+ Args:
29
+ driver: Sync database driver instance
30
+
31
+ Returns:
32
+ SQLite version information or None if detection fails
33
+ """
34
+ version_str = cast("SqliteDriver", 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
+ def get_feature_flag(self, driver: SyncDriverAdapterBase, feature: str) -> bool:
51
+ """Check if SQLite database supports a specific feature.
52
+
53
+ Args:
54
+ driver: SQLite driver instance
55
+ feature: Feature name to check
56
+
57
+ Returns:
58
+ True if feature is supported, False otherwise
59
+ """
60
+ version_info = self.get_version(driver)
61
+ if not version_info:
62
+ return False
63
+
64
+ feature_checks: dict[str, Callable[[VersionInfo], 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
+ def get_optimal_type(self, driver: SyncDriverAdapterBase, type_category: str) -> str:
83
+ """Get optimal SQLite type for a category.
84
+
85
+ Args:
86
+ driver: SQLite driver instance
87
+ type_category: Type category
88
+
89
+ Returns:
90
+ SQLite-specific type name
91
+ """
92
+ version_info = 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
+ def get_columns(
103
+ self, driver: SyncDriverAdapterBase, 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: SQLite 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
+ sqlite_driver = cast("SqliteDriver", driver)
120
+ result = sqlite_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,13 +4,24 @@ import contextlib
4
4
  import datetime
5
5
  import sqlite3
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
  from sqlspec.core.cache import get_cache_config
10
10
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
11
11
  from sqlspec.core.statement import StatementConfig
12
12
  from sqlspec.driver import SyncDriverAdapterBase
13
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
13
+ from sqlspec.exceptions import (
14
+ CheckViolationError,
15
+ DatabaseConnectionError,
16
+ DataError,
17
+ ForeignKeyViolationError,
18
+ IntegrityError,
19
+ NotNullViolationError,
20
+ OperationalError,
21
+ SQLParsingError,
22
+ SQLSpecError,
23
+ UniqueViolationError,
24
+ )
14
25
  from sqlspec.utils.serializers import to_json
15
26
 
16
27
  if TYPE_CHECKING:
@@ -20,9 +31,18 @@ if TYPE_CHECKING:
20
31
  from sqlspec.core.result import SQLResult
21
32
  from sqlspec.core.statement import SQL
22
33
  from sqlspec.driver import ExecutionResult
34
+ from sqlspec.driver._sync import SyncDataDictionaryBase
23
35
 
24
36
  __all__ = ("SqliteCursor", "SqliteDriver", "SqliteExceptionHandler", "sqlite_statement_config")
25
37
 
38
+ SQLITE_CONSTRAINT_UNIQUE_CODE = 2067
39
+ SQLITE_CONSTRAINT_FOREIGNKEY_CODE = 787
40
+ SQLITE_CONSTRAINT_NOTNULL_CODE = 1811
41
+ SQLITE_CONSTRAINT_CHECK_CODE = 531
42
+ SQLITE_CONSTRAINT_CODE = 19
43
+ SQLITE_CANTOPEN_CODE = 14
44
+ SQLITE_IOERR_CODE = 10
45
+ SQLITE_MISMATCH_CODE = 20
26
46
 
27
47
  sqlite_statement_config = StatementConfig(
28
48
  dialect="sqlite",
@@ -36,6 +56,7 @@ sqlite_statement_config = StatementConfig(
36
56
  datetime.datetime: lambda v: v.isoformat(),
37
57
  datetime.date: lambda v: v.isoformat(),
38
58
  Decimal: str,
59
+ dict: to_json,
39
60
  list: to_json,
40
61
  },
41
62
  has_native_list_expansion=False,
@@ -64,7 +85,7 @@ class SqliteCursor:
64
85
  connection: SQLite database connection
65
86
  """
66
87
  self.connection = connection
67
- self.cursor: Optional[sqlite3.Cursor] = None
88
+ self.cursor: sqlite3.Cursor | None = None
68
89
 
69
90
  def __enter__(self) -> "sqlite3.Cursor":
70
91
  """Create and return a new cursor.
@@ -75,7 +96,7 @@ class SqliteCursor:
75
96
  self.cursor = self.connection.cursor()
76
97
  return self.cursor
77
98
 
78
- def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
99
+ def __exit__(self, *_: Any) -> None:
79
100
  """Clean up cursor resources.
80
101
 
81
102
  Args:
@@ -83,7 +104,6 @@ class SqliteCursor:
83
104
  exc_val: Exception value if an exception occurred
84
105
  exc_tb: Exception traceback if an exception occurred
85
106
  """
86
- _ = (exc_type, exc_val, exc_tb)
87
107
  if self.cursor is not None:
88
108
  with contextlib.suppress(Exception):
89
109
  self.cursor.close()
@@ -92,64 +112,113 @@ class SqliteCursor:
92
112
  class SqliteExceptionHandler:
93
113
  """Context manager for handling SQLite database exceptions.
94
114
 
95
- Catches SQLite-specific exceptions and converts them to appropriate SQLSpec exceptions.
115
+ Maps SQLite extended result codes to specific SQLSpec exceptions
116
+ for better error handling in application code.
96
117
  """
97
118
 
98
119
  __slots__ = ()
99
120
 
100
121
  def __enter__(self) -> None:
101
- """Enter exception handling context.
102
-
103
- Returns:
104
- None
105
- """
106
- return
122
+ return None
107
123
 
108
124
  def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
109
- """Handle exceptions and convert to SQLSpec exceptions.
125
+ if exc_type is None:
126
+ return
127
+ if issubclass(exc_type, sqlite3.Error):
128
+ self._map_sqlite_exception(exc_val)
129
+
130
+ def _map_sqlite_exception(self, e: Any) -> None:
131
+ """Map SQLite exception to SQLSpec exception.
110
132
 
111
133
  Args:
112
- exc_type: Exception type if an exception occurred
113
- exc_val: Exception value if an exception occurred
114
- exc_tb: Exception traceback if an exception occurred
134
+ e: sqlite3.Error instance
115
135
 
116
136
  Raises:
117
- SQLSpecError: For general database errors
118
- SQLParsingError: For SQL syntax or parsing errors
137
+ Specific SQLSpec exception based on error code
119
138
  """
120
- if exc_type is None:
139
+ error_code = getattr(e, "sqlite_errorcode", None)
140
+ error_name = getattr(e, "sqlite_errorname", None)
141
+ error_msg = str(e).lower()
142
+
143
+ if "locked" in error_msg:
144
+ self._raise_operational_error(e, error_code or 0)
145
+
146
+ if not error_code:
147
+ if "unique constraint" in error_msg:
148
+ self._raise_unique_violation(e, 0)
149
+ elif "foreign key constraint" in error_msg:
150
+ self._raise_foreign_key_violation(e, 0)
151
+ elif "not null constraint" in error_msg:
152
+ self._raise_not_null_violation(e, 0)
153
+ elif "check constraint" in error_msg:
154
+ self._raise_check_violation(e, 0)
155
+ elif "syntax" in error_msg:
156
+ self._raise_parsing_error(e, None)
157
+ else:
158
+ self._raise_generic_error(e)
121
159
  return
122
160
 
123
- if issubclass(exc_type, sqlite3.IntegrityError):
124
- e = exc_val
125
- msg = f"SQLite integrity constraint violation: {e}"
126
- raise SQLSpecError(msg) from e
127
- if issubclass(exc_type, sqlite3.OperationalError):
128
- e = exc_val
129
- error_msg = str(e).lower()
130
- if "locked" in error_msg:
131
- raise
132
- if "syntax" in error_msg or "malformed" in error_msg:
133
- msg = f"SQLite SQL syntax error: {e}"
134
- raise SQLParsingError(msg) from e
135
- msg = f"SQLite operational error: {e}"
136
- raise SQLSpecError(msg) from e
137
- if issubclass(exc_type, sqlite3.DatabaseError):
138
- e = exc_val
139
- msg = f"SQLite database error: {e}"
140
- raise SQLSpecError(msg) from e
141
- if issubclass(exc_type, sqlite3.Error):
142
- e = exc_val
143
- msg = f"SQLite error: {e}"
144
- raise SQLSpecError(msg) from e
145
- if issubclass(exc_type, Exception):
146
- e = exc_val
147
- error_msg = str(e).lower()
148
- if "parse" in error_msg or "syntax" in error_msg:
149
- msg = f"SQL parsing failed: {e}"
150
- raise SQLParsingError(msg) from e
151
- msg = f"Unexpected database operation error: {e}"
152
- raise SQLSpecError(msg) from e
161
+ if error_code == SQLITE_CONSTRAINT_UNIQUE_CODE or error_name == "SQLITE_CONSTRAINT_UNIQUE":
162
+ self._raise_unique_violation(e, error_code)
163
+ elif error_code == SQLITE_CONSTRAINT_FOREIGNKEY_CODE or error_name == "SQLITE_CONSTRAINT_FOREIGNKEY":
164
+ self._raise_foreign_key_violation(e, error_code)
165
+ elif error_code == SQLITE_CONSTRAINT_NOTNULL_CODE or error_name == "SQLITE_CONSTRAINT_NOTNULL":
166
+ self._raise_not_null_violation(e, error_code)
167
+ elif error_code == SQLITE_CONSTRAINT_CHECK_CODE or error_name == "SQLITE_CONSTRAINT_CHECK":
168
+ self._raise_check_violation(e, error_code)
169
+ elif error_code == SQLITE_CONSTRAINT_CODE or error_name == "SQLITE_CONSTRAINT":
170
+ self._raise_integrity_error(e, error_code)
171
+ elif error_code == SQLITE_CANTOPEN_CODE or error_name == "SQLITE_CANTOPEN":
172
+ self._raise_connection_error(e, error_code)
173
+ elif error_code == SQLITE_IOERR_CODE or error_name == "SQLITE_IOERR":
174
+ self._raise_operational_error(e, error_code)
175
+ elif error_code == SQLITE_MISMATCH_CODE or error_name == "SQLITE_MISMATCH":
176
+ self._raise_data_error(e, error_code)
177
+ elif error_code == 1 or "syntax" in error_msg:
178
+ self._raise_parsing_error(e, error_code)
179
+ else:
180
+ self._raise_generic_error(e)
181
+
182
+ def _raise_unique_violation(self, e: Any, code: int) -> None:
183
+ msg = f"SQLite unique constraint violation [code {code}]: {e}"
184
+ raise UniqueViolationError(msg) from e
185
+
186
+ def _raise_foreign_key_violation(self, e: Any, code: int) -> None:
187
+ msg = f"SQLite foreign key constraint violation [code {code}]: {e}"
188
+ raise ForeignKeyViolationError(msg) from e
189
+
190
+ def _raise_not_null_violation(self, e: Any, code: int) -> None:
191
+ msg = f"SQLite not-null constraint violation [code {code}]: {e}"
192
+ raise NotNullViolationError(msg) from e
193
+
194
+ def _raise_check_violation(self, e: Any, code: int) -> None:
195
+ msg = f"SQLite check constraint violation [code {code}]: {e}"
196
+ raise CheckViolationError(msg) from e
197
+
198
+ def _raise_integrity_error(self, e: Any, code: int) -> None:
199
+ msg = f"SQLite integrity constraint violation [code {code}]: {e}"
200
+ raise IntegrityError(msg) from e
201
+
202
+ def _raise_parsing_error(self, e: Any, code: "int | None") -> None:
203
+ code_str = f"[code {code}]" if code else ""
204
+ msg = f"SQLite SQL syntax error {code_str}: {e}"
205
+ raise SQLParsingError(msg) from e
206
+
207
+ def _raise_connection_error(self, e: Any, code: int) -> None:
208
+ msg = f"SQLite connection error [code {code}]: {e}"
209
+ raise DatabaseConnectionError(msg) from e
210
+
211
+ def _raise_operational_error(self, e: Any, code: int) -> None:
212
+ msg = f"SQLite operational error [code {code}]: {e}"
213
+ raise OperationalError(msg) from e
214
+
215
+ def _raise_data_error(self, e: Any, code: int) -> None:
216
+ msg = f"SQLite data error [code {code}]: {e}"
217
+ raise DataError(msg) from e
218
+
219
+ def _raise_generic_error(self, e: Any) -> None:
220
+ msg = f"SQLite database error: {e}"
221
+ raise SQLSpecError(msg) from e
153
222
 
154
223
 
155
224
  class SqliteDriver(SyncDriverAdapterBase):
@@ -159,14 +228,14 @@ class SqliteDriver(SyncDriverAdapterBase):
159
228
  for SQLite databases using the standard sqlite3 module.
160
229
  """
161
230
 
162
- __slots__ = ()
231
+ __slots__ = ("_data_dictionary",)
163
232
  dialect = "sqlite"
164
233
 
165
234
  def __init__(
166
235
  self,
167
236
  connection: "SqliteConnection",
168
- statement_config: "Optional[StatementConfig]" = None,
169
- driver_features: "Optional[dict[str, Any]]" = None,
237
+ statement_config: "StatementConfig | None" = None,
238
+ driver_features: "dict[str, Any] | None" = None,
170
239
  ) -> None:
171
240
  """Initialize SQLite driver.
172
241
 
@@ -185,6 +254,7 @@ class SqliteDriver(SyncDriverAdapterBase):
185
254
  )
186
255
 
187
256
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
257
+ self._data_dictionary: SyncDataDictionaryBase | None = None
188
258
 
189
259
  def with_cursor(self, connection: "SqliteConnection") -> "SqliteCursor":
190
260
  """Create context manager for SQLite cursor.
@@ -205,7 +275,7 @@ class SqliteDriver(SyncDriverAdapterBase):
205
275
  """
206
276
  return SqliteExceptionHandler()
207
277
 
208
- def _try_special_handling(self, cursor: "sqlite3.Cursor", statement: "SQL") -> "Optional[SQLResult]":
278
+ def _try_special_handling(self, cursor: "sqlite3.Cursor", statement: "SQL") -> "SQLResult | None":
209
279
  """Hook for SQLite-specific special operations.
210
280
 
211
281
  Args:
@@ -280,7 +350,7 @@ class SqliteDriver(SyncDriverAdapterBase):
280
350
  fetched_data = cursor.fetchall()
281
351
  column_names = [col[0] for col in cursor.description or []]
282
352
 
283
- data = [dict(zip(column_names, row)) for row in fetched_data]
353
+ data = [dict(zip(column_names, row, strict=False)) for row in fetched_data]
284
354
 
285
355
  return self.create_execution_result(
286
356
  cursor, selected_data=data, column_names=column_names, data_row_count=len(data), is_select_result=True
@@ -325,3 +395,16 @@ class SqliteDriver(SyncDriverAdapterBase):
325
395
  except sqlite3.Error as e:
326
396
  msg = f"Failed to commit transaction: {e}"
327
397
  raise SQLSpecError(msg) from e
398
+
399
+ @property
400
+ def data_dictionary(self) -> "SyncDataDictionaryBase":
401
+ """Get the data dictionary for this driver.
402
+
403
+ Returns:
404
+ Data dictionary instance for metadata queries
405
+ """
406
+ if self._data_dictionary is None:
407
+ from sqlspec.adapters.sqlite.data_dictionary import SqliteSyncDataDictionary
408
+
409
+ self._data_dictionary = SqliteSyncDataDictionary()
410
+ return self._data_dictionary
@@ -0,0 +1,5 @@
1
+ """Litestar integration for SQLite adapter."""
2
+
3
+ from sqlspec.adapters.sqlite.litestar.store import SQLiteStore
4
+
5
+ __all__ = ("SQLiteStore",)