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
@@ -1,12 +1,14 @@
1
1
  """PostgreSQL-specific data dictionary for metadata queries via asyncpg."""
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.asyncpg.driver import AsyncpgDriver
11
13
 
12
14
  logger = get_logger("adapters.asyncpg.data_dictionary")
@@ -20,7 +22,7 @@ __all__ = ("PostgresAsyncDataDictionary",)
20
22
  class PostgresAsyncDataDictionary(AsyncDataDictionaryBase):
21
23
  """PostgreSQL-specific async data dictionary."""
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 PostgreSQL database version information.
25
27
 
26
28
  Args:
@@ -112,6 +114,43 @@ class PostgresAsyncDataDictionary(AsyncDataDictionaryBase):
112
114
  }
113
115
  return type_map.get(type_category, "TEXT")
114
116
 
117
+ async def get_columns(
118
+ self, driver: AsyncDriverAdapterBase, table: str, schema: "str | None" = None
119
+ ) -> "list[dict[str, Any]]":
120
+ """Get column information for a table using information_schema.
121
+
122
+ Args:
123
+ driver: AsyncPG driver instance
124
+ table: Table name to query columns for
125
+ schema: Schema name (None for default 'public')
126
+
127
+ Returns:
128
+ List of column metadata dictionaries with keys:
129
+ - column_name: Name of the column
130
+ - data_type: PostgreSQL data type
131
+ - is_nullable: Whether column allows NULL (YES/NO)
132
+ - column_default: Default value if any
133
+ """
134
+ asyncpg_driver = cast("AsyncpgDriver", driver)
135
+
136
+ if schema:
137
+ sql = f"""
138
+ SELECT column_name, data_type, is_nullable, column_default
139
+ FROM information_schema.columns
140
+ WHERE table_name = '{table}' AND table_schema = '{schema}'
141
+ ORDER BY ordinal_position
142
+ """
143
+ else:
144
+ sql = f"""
145
+ SELECT column_name, data_type, is_nullable, column_default
146
+ FROM information_schema.columns
147
+ WHERE table_name = '{table}' AND table_schema = 'public'
148
+ ORDER BY ordinal_position
149
+ """
150
+
151
+ result = await asyncpg_driver.execute(sql)
152
+ return result.data or []
153
+
115
154
  def list_available_features(self) -> "list[str]":
116
155
  """List available PostgreSQL feature flags.
117
156
 
@@ -6,7 +6,7 @@ PostgreSQL COPY operation support, and transaction management.
6
6
 
7
7
  import datetime
8
8
  import re
9
- from typing import TYPE_CHECKING, Any, Final, Optional
9
+ from typing import TYPE_CHECKING, Any, Final
10
10
 
11
11
  import asyncpg
12
12
 
@@ -14,7 +14,19 @@ from sqlspec.core.cache import get_cache_config
14
14
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
15
15
  from sqlspec.core.statement import StatementConfig
16
16
  from sqlspec.driver import AsyncDriverAdapterBase
17
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
17
+ from sqlspec.exceptions import (
18
+ CheckViolationError,
19
+ DatabaseConnectionError,
20
+ DataError,
21
+ ForeignKeyViolationError,
22
+ IntegrityError,
23
+ NotNullViolationError,
24
+ OperationalError,
25
+ SQLParsingError,
26
+ SQLSpecError,
27
+ TransactionError,
28
+ UniqueViolationError,
29
+ )
18
30
  from sqlspec.utils.logging import get_logger
19
31
 
20
32
  if TYPE_CHECKING:
@@ -31,6 +43,48 @@ __all__ = ("AsyncpgCursor", "AsyncpgDriver", "AsyncpgExceptionHandler", "asyncpg
31
43
  logger = get_logger("adapters.asyncpg")
32
44
 
33
45
 
46
+ def _convert_datetime_param(value: Any) -> Any:
47
+ """Convert datetime parameter, handling ISO strings.
48
+
49
+ Args:
50
+ value: datetime object or ISO format string
51
+
52
+ Returns:
53
+ datetime object for asyncpg
54
+ """
55
+ if isinstance(value, str):
56
+ return datetime.datetime.fromisoformat(value)
57
+ return value
58
+
59
+
60
+ def _convert_date_param(value: Any) -> Any:
61
+ """Convert date parameter, handling ISO strings.
62
+
63
+ Args:
64
+ value: date object or ISO format string
65
+
66
+ Returns:
67
+ date object for asyncpg
68
+ """
69
+ if isinstance(value, str):
70
+ return datetime.date.fromisoformat(value)
71
+ return value
72
+
73
+
74
+ def _convert_time_param(value: Any) -> Any:
75
+ """Convert time parameter, handling ISO strings.
76
+
77
+ Args:
78
+ value: time object or ISO format string
79
+
80
+ Returns:
81
+ time object for asyncpg
82
+ """
83
+ if isinstance(value, str):
84
+ return datetime.time.fromisoformat(value)
85
+ return value
86
+
87
+
34
88
  asyncpg_statement_config = StatementConfig(
35
89
  dialect="postgres",
36
90
  parameter_config=ParameterStyleConfig(
@@ -38,7 +92,11 @@ asyncpg_statement_config = StatementConfig(
38
92
  supported_parameter_styles={ParameterStyle.NUMERIC, ParameterStyle.POSITIONAL_PYFORMAT},
39
93
  default_execution_parameter_style=ParameterStyle.NUMERIC,
40
94
  supported_execution_parameter_styles={ParameterStyle.NUMERIC},
41
- type_coercion_map={datetime.datetime: lambda x: x, datetime.date: lambda x: x, datetime.time: lambda x: x},
95
+ type_coercion_map={
96
+ datetime.datetime: _convert_datetime_param,
97
+ datetime.date: _convert_date_param,
98
+ datetime.time: _convert_time_param,
99
+ },
42
100
  has_native_list_expansion=True,
43
101
  needs_static_script_compilation=False,
44
102
  preserve_parameter_format=True,
@@ -69,7 +127,11 @@ class AsyncpgCursor:
69
127
 
70
128
 
71
129
  class AsyncpgExceptionHandler:
72
- """Async context manager for handling AsyncPG database exceptions."""
130
+ """Async context manager for handling AsyncPG database exceptions.
131
+
132
+ Maps PostgreSQL SQLSTATE error codes to specific SQLSpec exceptions
133
+ for better error handling in application code.
134
+ """
73
135
 
74
136
  __slots__ = ()
75
137
 
@@ -80,21 +142,89 @@ class AsyncpgExceptionHandler:
80
142
  if exc_type is None:
81
143
  return
82
144
  if issubclass(exc_type, asyncpg.PostgresError):
83
- e = exc_val
84
- error_code = getattr(e, "sqlstate", None)
85
- if error_code:
86
- if error_code.startswith("23"):
87
- msg = f"PostgreSQL integrity constraint violation [{error_code}]: {e}"
88
- elif error_code.startswith("42"):
89
- msg = f"PostgreSQL SQL syntax error [{error_code}]: {e}"
90
- raise SQLParsingError(msg) from e
91
- elif error_code.startswith("08"):
92
- msg = f"PostgreSQL connection error [{error_code}]: {e}"
93
- else:
94
- msg = f"PostgreSQL database error [{error_code}]: {e}"
95
- else:
96
- msg = f"PostgreSQL database error: {e}"
97
- raise SQLSpecError(msg) from e
145
+ self._map_postgres_exception(exc_val)
146
+
147
+ def _map_postgres_exception(self, e: Any) -> None:
148
+ """Map PostgreSQL exception to SQLSpec exception.
149
+
150
+ Args:
151
+ e: asyncpg.PostgresError instance
152
+
153
+ Raises:
154
+ Specific SQLSpec exception based on SQLSTATE code
155
+ """
156
+ error_code = getattr(e, "sqlstate", None)
157
+
158
+ if not error_code:
159
+ self._raise_generic_error(e, None)
160
+ return
161
+
162
+ if error_code == "23505":
163
+ self._raise_unique_violation(e, error_code)
164
+ elif error_code == "23503":
165
+ self._raise_foreign_key_violation(e, error_code)
166
+ elif error_code == "23502":
167
+ self._raise_not_null_violation(e, error_code)
168
+ elif error_code == "23514":
169
+ self._raise_check_violation(e, error_code)
170
+ elif error_code.startswith("23"):
171
+ self._raise_integrity_error(e, error_code)
172
+ elif error_code.startswith("42"):
173
+ self._raise_parsing_error(e, error_code)
174
+ elif error_code.startswith("08"):
175
+ self._raise_connection_error(e, error_code)
176
+ elif error_code.startswith("40"):
177
+ self._raise_transaction_error(e, error_code)
178
+ elif error_code.startswith("22"):
179
+ self._raise_data_error(e, error_code)
180
+ elif error_code.startswith(("53", "54", "55", "57", "58")):
181
+ self._raise_operational_error(e, error_code)
182
+ else:
183
+ self._raise_generic_error(e, error_code)
184
+
185
+ def _raise_unique_violation(self, e: Any, code: str) -> None:
186
+ msg = f"PostgreSQL unique constraint violation [{code}]: {e}"
187
+ raise UniqueViolationError(msg) from e
188
+
189
+ def _raise_foreign_key_violation(self, e: Any, code: str) -> None:
190
+ msg = f"PostgreSQL foreign key constraint violation [{code}]: {e}"
191
+ raise ForeignKeyViolationError(msg) from e
192
+
193
+ def _raise_not_null_violation(self, e: Any, code: str) -> None:
194
+ msg = f"PostgreSQL not-null constraint violation [{code}]: {e}"
195
+ raise NotNullViolationError(msg) from e
196
+
197
+ def _raise_check_violation(self, e: Any, code: str) -> None:
198
+ msg = f"PostgreSQL check constraint violation [{code}]: {e}"
199
+ raise CheckViolationError(msg) from e
200
+
201
+ def _raise_integrity_error(self, e: Any, code: str) -> None:
202
+ msg = f"PostgreSQL integrity constraint violation [{code}]: {e}"
203
+ raise IntegrityError(msg) from e
204
+
205
+ def _raise_parsing_error(self, e: Any, code: str) -> None:
206
+ msg = f"PostgreSQL SQL syntax error [{code}]: {e}"
207
+ raise SQLParsingError(msg) from e
208
+
209
+ def _raise_connection_error(self, e: Any, code: str) -> None:
210
+ msg = f"PostgreSQL connection error [{code}]: {e}"
211
+ raise DatabaseConnectionError(msg) from e
212
+
213
+ def _raise_transaction_error(self, e: Any, code: str) -> None:
214
+ msg = f"PostgreSQL transaction error [{code}]: {e}"
215
+ raise TransactionError(msg) from e
216
+
217
+ def _raise_data_error(self, e: Any, code: str) -> None:
218
+ msg = f"PostgreSQL data error [{code}]: {e}"
219
+ raise DataError(msg) from e
220
+
221
+ def _raise_operational_error(self, e: Any, code: str) -> None:
222
+ msg = f"PostgreSQL operational error [{code}]: {e}"
223
+ raise OperationalError(msg) from e
224
+
225
+ def _raise_generic_error(self, e: Any, code: "str | None") -> None:
226
+ msg = f"PostgreSQL database error [{code}]: {e}" if code else f"PostgreSQL database error: {e}"
227
+ raise SQLSpecError(msg) from e
98
228
 
99
229
 
100
230
  class AsyncpgDriver(AsyncDriverAdapterBase):
@@ -111,8 +241,8 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
111
241
  def __init__(
112
242
  self,
113
243
  connection: "AsyncpgConnection",
114
- statement_config: "Optional[StatementConfig]" = None,
115
- driver_features: "Optional[dict[str, Any]]" = None,
244
+ statement_config: "StatementConfig | None" = None,
245
+ driver_features: "dict[str, Any] | None" = None,
116
246
  ) -> None:
117
247
  if statement_config is None:
118
248
  cache_config = get_cache_config()
@@ -124,7 +254,7 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
124
254
  )
125
255
 
126
256
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
127
- self._data_dictionary: Optional[AsyncDataDictionaryBase] = None
257
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
128
258
 
129
259
  def with_cursor(self, connection: "AsyncpgConnection") -> "AsyncpgCursor":
130
260
  """Create context manager for AsyncPG cursor."""
@@ -134,7 +264,7 @@ class AsyncpgDriver(AsyncDriverAdapterBase):
134
264
  """Handle database exceptions with PostgreSQL error codes."""
135
265
  return AsyncpgExceptionHandler()
136
266
 
137
- async def _try_special_handling(self, cursor: "AsyncpgConnection", statement: "SQL") -> "Optional[SQLResult]":
267
+ async def _try_special_handling(self, cursor: "AsyncpgConnection", statement: "SQL") -> "SQLResult | None":
138
268
  """Handle PostgreSQL COPY operations and other special cases.
139
269
 
140
270
  Args:
@@ -0,0 +1,5 @@
1
+ """Litestar integration for AsyncPG adapter."""
2
+
3
+ from sqlspec.adapters.asyncpg.litestar.store import AsyncpgStore
4
+
5
+ __all__ = ("AsyncpgStore",)
@@ -0,0 +1,253 @@
1
+ """AsyncPG session store for Litestar integration."""
2
+
3
+ from datetime import datetime, timedelta, timezone
4
+ from typing import TYPE_CHECKING
5
+
6
+ from sqlspec.extensions.litestar.store import BaseSQLSpecStore
7
+ from sqlspec.utils.logging import get_logger
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlspec.adapters.asyncpg.config import AsyncpgConfig
11
+
12
+ logger = get_logger("adapters.asyncpg.litestar.store")
13
+
14
+ __all__ = ("AsyncpgStore",)
15
+
16
+
17
+ class AsyncpgStore(BaseSQLSpecStore["AsyncpgConfig"]):
18
+ """PostgreSQL session store using AsyncPG driver.
19
+
20
+ Implements server-side session storage for Litestar using PostgreSQL
21
+ via the AsyncPG driver. Provides efficient session management with:
22
+ - Native async PostgreSQL operations
23
+ - UPSERT support using ON CONFLICT
24
+ - Automatic expiration handling
25
+ - Efficient cleanup of expired sessions
26
+
27
+ Args:
28
+ config: AsyncpgConfig instance with extension_config["litestar"] settings.
29
+
30
+ Example:
31
+ from sqlspec.adapters.asyncpg import AsyncpgConfig
32
+ from sqlspec.adapters.asyncpg.litestar.store import AsyncpgStore
33
+
34
+ config = AsyncpgConfig(
35
+ pool_config={"dsn": "postgresql://..."},
36
+ extension_config={"litestar": {"session_table": "my_sessions"}}
37
+ )
38
+ store = AsyncpgStore(config)
39
+ await store.create_table()
40
+ """
41
+
42
+ __slots__ = ()
43
+
44
+ def __init__(self, config: "AsyncpgConfig") -> None:
45
+ """Initialize AsyncPG session store.
46
+
47
+ Args:
48
+ config: AsyncpgConfig instance.
49
+
50
+ Notes:
51
+ Table name is read from config.extension_config["litestar"]["session_table"].
52
+ """
53
+ super().__init__(config)
54
+
55
+ def _get_create_table_sql(self) -> str:
56
+ """Get PostgreSQL CREATE TABLE SQL with optimized schema.
57
+
58
+ Returns:
59
+ SQL statement to create the sessions table with proper indexes.
60
+
61
+ Notes:
62
+ - Uses TIMESTAMPTZ for timezone-aware expiration timestamps
63
+ - Partial index WHERE expires_at IS NOT NULL reduces index size/maintenance
64
+ - FILLFACTOR 80 leaves space for HOT updates, reducing table bloat
65
+ - Audit columns (created_at, updated_at) help with debugging
66
+ - Table name is internally controlled, not user input (S608 suppressed)
67
+ """
68
+ return f"""
69
+ CREATE TABLE IF NOT EXISTS {self._table_name} (
70
+ session_id TEXT PRIMARY KEY,
71
+ data BYTEA NOT NULL,
72
+ expires_at TIMESTAMPTZ,
73
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
74
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
75
+ ) WITH (fillfactor = 80);
76
+
77
+ CREATE INDEX IF NOT EXISTS idx_{self._table_name}_expires_at
78
+ ON {self._table_name}(expires_at) WHERE expires_at IS NOT NULL;
79
+
80
+ ALTER TABLE {self._table_name} SET (
81
+ autovacuum_vacuum_scale_factor = 0.05,
82
+ autovacuum_analyze_scale_factor = 0.02
83
+ );
84
+ """
85
+
86
+ def _get_drop_table_sql(self) -> "list[str]":
87
+ """Get PostgreSQL DROP TABLE SQL statements.
88
+
89
+ Returns:
90
+ List of SQL statements to drop indexes and table.
91
+ """
92
+ return [f"DROP INDEX IF EXISTS idx_{self._table_name}_expires_at", f"DROP TABLE IF EXISTS {self._table_name}"]
93
+
94
+ async def create_table(self) -> None:
95
+ """Create the session table if it doesn't exist."""
96
+ sql = self._get_create_table_sql()
97
+ async with self._config.provide_session() as driver:
98
+ await driver.execute_script(sql)
99
+ logger.debug("Created session table: %s", self._table_name)
100
+
101
+ async def get(self, key: str, renew_for: "int | timedelta | None" = None) -> "bytes | None":
102
+ """Get a session value by key.
103
+
104
+ Args:
105
+ key: Session ID to retrieve.
106
+ renew_for: If given, renew the expiry time for this duration.
107
+
108
+ Returns:
109
+ Session data as bytes if found and not expired, None otherwise.
110
+
111
+ Notes:
112
+ Uses CURRENT_TIMESTAMP instead of NOW() for SQL standard compliance.
113
+ The query planner can use the partial index for expires_at > CURRENT_TIMESTAMP.
114
+ """
115
+ sql = f"""
116
+ SELECT data, expires_at FROM {self._table_name}
117
+ WHERE session_id = $1
118
+ AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
119
+ """
120
+
121
+ async with self._config.provide_connection() as conn:
122
+ row = await conn.fetchrow(sql, key)
123
+
124
+ if row is None:
125
+ return None
126
+
127
+ if renew_for is not None and row["expires_at"] is not None:
128
+ new_expires_at = self._calculate_expires_at(renew_for)
129
+ if new_expires_at is not None:
130
+ update_sql = f"""
131
+ UPDATE {self._table_name}
132
+ SET expires_at = $1, updated_at = CURRENT_TIMESTAMP
133
+ WHERE session_id = $2
134
+ """
135
+ await conn.execute(update_sql, new_expires_at, key)
136
+
137
+ return bytes(row["data"])
138
+
139
+ async def set(self, key: str, value: "str | bytes", expires_in: "int | timedelta | None" = None) -> None:
140
+ """Store a session value.
141
+
142
+ Args:
143
+ key: Session ID.
144
+ value: Session data.
145
+ expires_in: Time until expiration.
146
+
147
+ Notes:
148
+ Uses EXCLUDED to reference the proposed insert values in ON CONFLICT.
149
+ Updates updated_at timestamp on every write for audit trail.
150
+ """
151
+ data = self._value_to_bytes(value)
152
+ expires_at = self._calculate_expires_at(expires_in)
153
+
154
+ sql = f"""
155
+ INSERT INTO {self._table_name} (session_id, data, expires_at)
156
+ VALUES ($1, $2, $3)
157
+ ON CONFLICT (session_id)
158
+ DO UPDATE SET
159
+ data = EXCLUDED.data,
160
+ expires_at = EXCLUDED.expires_at,
161
+ updated_at = CURRENT_TIMESTAMP
162
+ """
163
+
164
+ async with self._config.provide_connection() as conn:
165
+ await conn.execute(sql, key, data, expires_at)
166
+
167
+ async def delete(self, key: str) -> None:
168
+ """Delete a session by key.
169
+
170
+ Args:
171
+ key: Session ID to delete.
172
+ """
173
+ sql = f"DELETE FROM {self._table_name} WHERE session_id = $1"
174
+
175
+ async with self._config.provide_connection() as conn:
176
+ await conn.execute(sql, key)
177
+
178
+ async def delete_all(self) -> None:
179
+ """Delete all sessions from the store."""
180
+ sql = f"DELETE FROM {self._table_name}"
181
+
182
+ async with self._config.provide_connection() as conn:
183
+ await conn.execute(sql)
184
+ logger.debug("Deleted all sessions from table: %s", self._table_name)
185
+
186
+ async def exists(self, key: str) -> bool:
187
+ """Check if a session key exists and is not expired.
188
+
189
+ Args:
190
+ key: Session ID to check.
191
+
192
+ Returns:
193
+ True if the session exists and is not expired.
194
+
195
+ Notes:
196
+ Uses CURRENT_TIMESTAMP for consistency with get() method.
197
+ """
198
+ sql = f"""
199
+ SELECT 1 FROM {self._table_name}
200
+ WHERE session_id = $1
201
+ AND (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
202
+ """
203
+
204
+ async with self._config.provide_connection() as conn:
205
+ result = await conn.fetchval(sql, key)
206
+ return result is not None
207
+
208
+ async def expires_in(self, key: str) -> "int | None":
209
+ """Get the time in seconds until the session expires.
210
+
211
+ Args:
212
+ key: Session ID to check.
213
+
214
+ Returns:
215
+ Seconds until expiration, or None if no expiry or key doesn't exist.
216
+ """
217
+ sql = f"""
218
+ SELECT expires_at FROM {self._table_name}
219
+ WHERE session_id = $1
220
+ """
221
+
222
+ async with self._config.provide_connection() as conn:
223
+ expires_at = await conn.fetchval(sql, key)
224
+
225
+ if expires_at is None:
226
+ return None
227
+
228
+ now = datetime.now(timezone.utc)
229
+ if expires_at <= now:
230
+ return 0
231
+
232
+ delta = expires_at - now
233
+ return int(delta.total_seconds())
234
+
235
+ async def delete_expired(self) -> int:
236
+ """Delete all expired sessions.
237
+
238
+ Returns:
239
+ Number of sessions deleted.
240
+
241
+ Notes:
242
+ Uses CURRENT_TIMESTAMP for consistency.
243
+ For very large tables (10M+ rows), consider batching deletes
244
+ to avoid holding locks too long.
245
+ """
246
+ sql = f"DELETE FROM {self._table_name} WHERE expires_at <= CURRENT_TIMESTAMP"
247
+
248
+ async with self._config.provide_connection() as conn:
249
+ result = await conn.execute(sql)
250
+ count = int(result.split()[-1])
251
+ if count > 0:
252
+ logger.debug("Cleaned up %d expired sessions", count)
253
+ return count
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING
3
3
  from google.cloud.bigquery import Client
4
4
 
5
5
  if TYPE_CHECKING:
6
- from typing_extensions import TypeAlias
6
+ from typing import TypeAlias
7
7
 
8
8
  BigQueryConnection: TypeAlias = Client
9
9
  else:
@@ -0,0 +1,5 @@
1
+ """BigQuery ADK store for Google Agent Development Kit session/event storage."""
2
+
3
+ from sqlspec.adapters.bigquery.adk.store import BigQueryADKStore
4
+
5
+ __all__ = ("BigQueryADKStore",)