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
@@ -0,0 +1,172 @@
1
+ """PostgreSQL-specific data dictionary for metadata queries via psqlpy."""
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.psqlpy.driver import PsqlpyDriver
13
+
14
+ logger = get_logger("adapters.psqlpy.data_dictionary")
15
+
16
+ # Compiled regex patterns
17
+ POSTGRES_VERSION_PATTERN = re.compile(r"PostgreSQL (\d+)\.(\d+)(?:\.(\d+))?")
18
+
19
+ __all__ = ("PsqlpyAsyncDataDictionary",)
20
+
21
+
22
+ class PsqlpyAsyncDataDictionary(AsyncDataDictionaryBase):
23
+ """PostgreSQL-specific async data dictionary via psqlpy."""
24
+
25
+ async def get_version(self, driver: AsyncDriverAdapterBase) -> "VersionInfo | None":
26
+ """Get PostgreSQL database version information.
27
+
28
+ Args:
29
+ driver: Async database driver instance
30
+
31
+ Returns:
32
+ PostgreSQL version information or None if detection fails
33
+ """
34
+ version_str = await cast("PsqlpyDriver", driver).select_value("SELECT version()")
35
+ if not version_str:
36
+ logger.warning("No PostgreSQL version information found")
37
+ return None
38
+
39
+ # Parse version like "PostgreSQL 15.3 on x86_64-pc-linux-gnu..."
40
+ version_match = POSTGRES_VERSION_PATTERN.search(str(version_str))
41
+ if not version_match:
42
+ logger.warning("Could not parse PostgreSQL version: %s", version_str)
43
+ return None
44
+
45
+ major = int(version_match.group(1))
46
+ minor = int(version_match.group(2))
47
+ patch = int(version_match.group(3)) if version_match.group(3) else 0
48
+
49
+ version_info = VersionInfo(major, minor, patch)
50
+ logger.debug("Detected PostgreSQL version: %s", version_info)
51
+ return version_info
52
+
53
+ async def get_feature_flag(self, driver: AsyncDriverAdapterBase, feature: str) -> bool:
54
+ """Check if PostgreSQL database supports a specific feature.
55
+
56
+ Args:
57
+ driver: Async database driver instance
58
+ feature: Feature name to check
59
+
60
+ Returns:
61
+ True if feature is supported, False otherwise
62
+ """
63
+ version_info = await self.get_version(driver)
64
+ if not version_info:
65
+ return False
66
+
67
+ feature_checks: dict[str, Callable[[VersionInfo], bool]] = {
68
+ "supports_json": lambda v: v >= VersionInfo(9, 2, 0),
69
+ "supports_jsonb": lambda v: v >= VersionInfo(9, 4, 0),
70
+ "supports_uuid": lambda _: True, # UUID extension widely available
71
+ "supports_arrays": lambda _: True, # PostgreSQL has excellent array support
72
+ "supports_returning": lambda v: v >= VersionInfo(8, 2, 0),
73
+ "supports_upsert": lambda v: v >= VersionInfo(9, 5, 0), # ON CONFLICT
74
+ "supports_window_functions": lambda v: v >= VersionInfo(8, 4, 0),
75
+ "supports_cte": lambda v: v >= VersionInfo(8, 4, 0),
76
+ "supports_transactions": lambda _: True,
77
+ "supports_prepared_statements": lambda _: True,
78
+ "supports_schemas": lambda _: True,
79
+ "supports_partitioning": lambda v: v >= VersionInfo(10, 0, 0),
80
+ }
81
+
82
+ if feature in feature_checks:
83
+ return bool(feature_checks[feature](version_info))
84
+
85
+ return False
86
+
87
+ async def get_optimal_type(self, driver: AsyncDriverAdapterBase, type_category: str) -> str:
88
+ """Get optimal PostgreSQL type for a category.
89
+
90
+ Args:
91
+ driver: Async database driver instance
92
+ type_category: Type category
93
+
94
+ Returns:
95
+ PostgreSQL-specific type name
96
+ """
97
+ version_info = await self.get_version(driver)
98
+
99
+ if type_category == "json":
100
+ if version_info and version_info >= VersionInfo(9, 4, 0):
101
+ return "JSONB" # Prefer JSONB over JSON
102
+ if version_info and version_info >= VersionInfo(9, 2, 0):
103
+ return "JSON"
104
+ return "TEXT"
105
+
106
+ type_map = {
107
+ "uuid": "UUID",
108
+ "boolean": "BOOLEAN",
109
+ "timestamp": "TIMESTAMP WITH TIME ZONE",
110
+ "text": "TEXT",
111
+ "blob": "BYTEA",
112
+ "array": "ARRAY",
113
+ }
114
+ return type_map.get(type_category, "TEXT")
115
+
116
+ async def get_columns(
117
+ self, driver: AsyncDriverAdapterBase, table: str, schema: "str | None" = None
118
+ ) -> "list[dict[str, Any]]":
119
+ """Get column information for a table using information_schema.
120
+
121
+ Args:
122
+ driver: Psqlpy async driver instance
123
+ table: Table name to query columns for
124
+ schema: Schema name (None for default 'public')
125
+
126
+ Returns:
127
+ List of column metadata dictionaries with keys:
128
+ - column_name: Name of the column
129
+ - data_type: PostgreSQL data type
130
+ - is_nullable: Whether column allows NULL (YES/NO)
131
+ - column_default: Default value if any
132
+ """
133
+ psqlpy_driver = cast("PsqlpyDriver", driver)
134
+
135
+ if schema:
136
+ sql = f"""
137
+ SELECT column_name, data_type, is_nullable, column_default
138
+ FROM information_schema.columns
139
+ WHERE table_name = '{table}' AND table_schema = '{schema}'
140
+ ORDER BY ordinal_position
141
+ """
142
+ else:
143
+ sql = f"""
144
+ SELECT column_name, data_type, is_nullable, column_default
145
+ FROM information_schema.columns
146
+ WHERE table_name = '{table}' AND table_schema = 'public'
147
+ ORDER BY ordinal_position
148
+ """
149
+
150
+ result = await psqlpy_driver.execute(sql)
151
+ return result.data or []
152
+
153
+ def list_available_features(self) -> "list[str]":
154
+ """List available PostgreSQL feature flags.
155
+
156
+ Returns:
157
+ List of supported feature names
158
+ """
159
+ return [
160
+ "supports_json",
161
+ "supports_jsonb",
162
+ "supports_uuid",
163
+ "supports_arrays",
164
+ "supports_returning",
165
+ "supports_upsert",
166
+ "supports_window_functions",
167
+ "supports_cte",
168
+ "supports_transactions",
169
+ "supports_prepared_statements",
170
+ "supports_schemas",
171
+ "supports_partitioning",
172
+ ]
@@ -4,20 +4,31 @@ Provides parameter style conversion, type coercion, error handling,
4
4
  and transaction management.
5
5
  """
6
6
 
7
- import datetime
8
7
  import decimal
9
8
  import re
10
- import uuid
11
- from typing import TYPE_CHECKING, Any, Final, Optional
9
+ from typing import TYPE_CHECKING, Any, Final
12
10
 
13
11
  import psqlpy
14
12
  import psqlpy.exceptions
15
13
 
14
+ from sqlspec.adapters.psqlpy.type_converter import PostgreSQLTypeConverter
16
15
  from sqlspec.core.cache import get_cache_config
17
16
  from sqlspec.core.parameters import ParameterStyle, ParameterStyleConfig
18
17
  from sqlspec.core.statement import SQL, StatementConfig
19
18
  from sqlspec.driver import AsyncDriverAdapterBase
20
- from sqlspec.exceptions import SQLParsingError, SQLSpecError
19
+ from sqlspec.exceptions import (
20
+ CheckViolationError,
21
+ DatabaseConnectionError,
22
+ DataError,
23
+ ForeignKeyViolationError,
24
+ IntegrityError,
25
+ NotNullViolationError,
26
+ OperationalError,
27
+ SQLParsingError,
28
+ SQLSpecError,
29
+ TransactionError,
30
+ UniqueViolationError,
31
+ )
21
32
  from sqlspec.utils.logging import get_logger
22
33
 
23
34
  if TYPE_CHECKING:
@@ -26,11 +37,14 @@ if TYPE_CHECKING:
26
37
  from sqlspec.adapters.psqlpy._types import PsqlpyConnection
27
38
  from sqlspec.core.result import SQLResult
28
39
  from sqlspec.driver import ExecutionResult
40
+ from sqlspec.driver._async import AsyncDataDictionaryBase
29
41
 
30
42
  __all__ = ("PsqlpyCursor", "PsqlpyDriver", "PsqlpyExceptionHandler", "psqlpy_statement_config")
31
43
 
32
44
  logger = get_logger("adapters.psqlpy")
33
45
 
46
+ _type_converter = PostgreSQLTypeConverter()
47
+
34
48
  psqlpy_statement_config = StatementConfig(
35
49
  dialect="postgres",
36
50
  parameter_config=ParameterStyleConfig(
@@ -38,7 +52,7 @@ psqlpy_statement_config = StatementConfig(
38
52
  supported_parameter_styles={ParameterStyle.NUMERIC, ParameterStyle.NAMED_DOLLAR, ParameterStyle.QMARK},
39
53
  default_execution_parameter_style=ParameterStyle.NUMERIC,
40
54
  supported_execution_parameter_styles={ParameterStyle.NUMERIC},
41
- type_coercion_map={tuple: list, decimal.Decimal: float},
55
+ type_coercion_map={tuple: list, decimal.Decimal: float, str: _type_converter.convert_if_detected},
42
56
  has_native_list_expansion=False,
43
57
  needs_static_script_compilation=False,
44
58
  allow_mixed_parameter_styles=False,
@@ -52,173 +66,6 @@ psqlpy_statement_config = StatementConfig(
52
66
 
53
67
  PSQLPY_STATUS_REGEX: Final[re.Pattern[str]] = re.compile(r"^([A-Z]+)(?:\s+(\d+))?\s+(\d+)$", re.IGNORECASE)
54
68
 
55
- SPECIAL_TYPE_REGEX: Final[re.Pattern[str]] = re.compile(
56
- r"^(?:"
57
- r"(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|"
58
- r"(?P<ipv4>(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/(?:3[0-2]|[12]?[0-9]))?)|"
59
- r"(?P<ipv6>(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,7}:|:(?::[0-9a-f]{1,4}){1,7}|(?:[0-9a-f]{1,4}:){1,6}:[0-9a-f]{1,4}|::(?:ffff:)?(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?:/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9]))?)|"
60
- r"(?P<mac>(?:[0-9a-f]{2}[:-]){5}[0-9a-f]{2}|[0-9a-f]{12})|"
61
- r"(?P<iso_datetime>\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:?\d{2})?)|"
62
- r"(?P<iso_date>\d{4}-\d{2}-\d{2})|"
63
- r"(?P<iso_time>\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?(?:Z|[+-]\d{2}:?\d{2})?)|"
64
- r"(?P<interval>(?:(?:\d+\s+(?:year|month|day|hour|minute|second)s?\s*)+)|(?:P(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?))|"
65
- r"(?P<json>\{[\s\S]*\}|\[[\s\S]*\])|"
66
- r"(?P<pg_array>\{(?:[^{}]+|\{[^{}]*\})*\})"
67
- r")$",
68
- re.IGNORECASE,
69
- )
70
-
71
-
72
- def _detect_postgresql_type(value: str) -> Optional[str]:
73
- """Detect PostgreSQL data type from string value.
74
-
75
- Args:
76
- value: String value to analyze
77
-
78
- Returns:
79
- Type name if detected, None otherwise.
80
- """
81
- match = SPECIAL_TYPE_REGEX.match(value)
82
- if not match:
83
- return None
84
-
85
- for group_name in [
86
- "uuid",
87
- "ipv4",
88
- "ipv6",
89
- "mac",
90
- "iso_datetime",
91
- "iso_date",
92
- "iso_time",
93
- "interval",
94
- "json",
95
- "pg_array",
96
- ]:
97
- if match.group(group_name):
98
- return group_name
99
-
100
- return None
101
-
102
-
103
- def _convert_uuid(value: str) -> Any:
104
- """Convert UUID string to UUID object.
105
-
106
- Args:
107
- value: UUID string to convert
108
-
109
- Returns:
110
- UUID object or original value if conversion fails
111
- """
112
- try:
113
- clean_uuid = value.replace("-", "").lower()
114
- uuid_length = 32
115
- if len(clean_uuid) == uuid_length:
116
- formatted = f"{clean_uuid[:8]}-{clean_uuid[8:12]}-{clean_uuid[12:16]}-{clean_uuid[16:20]}-{clean_uuid[20:]}"
117
- return uuid.UUID(formatted)
118
- return uuid.UUID(value)
119
- except (ValueError, AttributeError):
120
- return value
121
-
122
-
123
- def _convert_iso_datetime(value: str) -> Any:
124
- """Convert ISO datetime string to datetime object.
125
-
126
- Args:
127
- value: ISO datetime string to convert
128
-
129
- Returns:
130
- datetime object or original value if conversion fails
131
- """
132
- try:
133
- normalized = value.replace("Z", "+00:00")
134
- return datetime.datetime.fromisoformat(normalized)
135
- except ValueError:
136
- return value
137
-
138
-
139
- def _convert_iso_date(value: str) -> Any:
140
- """Convert ISO date string to date object.
141
-
142
- Args:
143
- value: ISO date string to convert
144
-
145
- Returns:
146
- date object or original value if conversion fails
147
- """
148
- try:
149
- return datetime.date.fromisoformat(value)
150
- except ValueError:
151
- return value
152
-
153
-
154
- def _validate_json(value: str) -> str:
155
- """Validate JSON string format.
156
-
157
- Args:
158
- value: JSON string to validate
159
-
160
- Returns:
161
- Original string value
162
- """
163
- from sqlspec.utils.serializers import from_json
164
-
165
- try:
166
- from_json(value)
167
- except (ValueError, TypeError):
168
- return value
169
- return value
170
-
171
-
172
- def _passthrough(value: str) -> str:
173
- """Pass value through unchanged.
174
-
175
- Args:
176
- value: String value to pass through
177
-
178
- Returns:
179
- Original value unchanged
180
- """
181
- return value
182
-
183
-
184
- _PSQLPY_TYPE_CONVERTERS: dict[str, Any] = {
185
- "uuid": _convert_uuid,
186
- "iso_datetime": _convert_iso_datetime,
187
- "iso_date": _convert_iso_date,
188
- "iso_time": _passthrough,
189
- "json": _validate_json,
190
- "pg_array": _passthrough,
191
- "ipv4": _passthrough,
192
- "ipv6": _passthrough,
193
- "mac": _passthrough,
194
- "interval": _passthrough,
195
- }
196
-
197
-
198
- def _convert_psqlpy_parameters(value: Any) -> Any:
199
- """Convert parameters for psqlpy compatibility.
200
-
201
- Args:
202
- value: Parameter value to convert
203
-
204
- Returns:
205
- Converted value suitable for psqlpy execution
206
- """
207
- if isinstance(value, str):
208
- detected_type = _detect_postgresql_type(value)
209
-
210
- if detected_type:
211
- converter = _PSQLPY_TYPE_CONVERTERS.get(detected_type)
212
- if converter:
213
- return converter(value)
214
-
215
- return value
216
-
217
- if isinstance(value, (dict, list, tuple, uuid.UUID, datetime.datetime, datetime.date)):
218
- return value
219
-
220
- return value
221
-
222
69
 
223
70
  class PsqlpyCursor:
224
71
  """Context manager for psqlpy cursor management."""
@@ -238,7 +85,7 @@ class PsqlpyCursor:
238
85
  self._in_use = True
239
86
  return self.connection
240
87
 
241
- async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
88
+ async def __aexit__(self, *_: Any) -> None:
242
89
  """Exit cursor context.
243
90
 
244
91
  Args:
@@ -246,7 +93,6 @@ class PsqlpyCursor:
246
93
  exc_val: Exception value
247
94
  exc_tb: Exception traceback
248
95
  """
249
- _ = (exc_type, exc_val, exc_tb)
250
96
  self._in_use = False
251
97
 
252
98
  def is_in_use(self) -> bool:
@@ -259,7 +105,11 @@ class PsqlpyCursor:
259
105
 
260
106
 
261
107
  class PsqlpyExceptionHandler:
262
- """Async context manager for handling psqlpy database exceptions."""
108
+ """Async context manager for handling psqlpy database exceptions.
109
+
110
+ Maps PostgreSQL SQLSTATE error codes to specific SQLSpec exceptions
111
+ for better error handling in application code.
112
+ """
263
113
 
264
114
  __slots__ = ()
265
115
 
@@ -269,31 +119,85 @@ class PsqlpyExceptionHandler:
269
119
  async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
270
120
  if exc_type is None:
271
121
  return
122
+ if issubclass(exc_type, (psqlpy.exceptions.DatabaseError, psqlpy.exceptions.Error)):
123
+ self._map_postgres_exception(exc_val)
272
124
 
273
- if issubclass(exc_type, psqlpy.exceptions.DatabaseError):
274
- e = exc_val
275
- msg = f"Psqlpy database error: {e}"
276
- raise SQLSpecError(msg) from e
277
- if issubclass(exc_type, psqlpy.exceptions.InterfaceError):
278
- e = exc_val
279
- msg = f"Psqlpy interface error: {e}"
280
- raise SQLSpecError(msg) from e
281
- if issubclass(exc_type, psqlpy.exceptions.Error):
282
- e = exc_val
283
- error_msg = str(e).lower()
284
- if "syntax" in error_msg or "parse" in error_msg:
285
- msg = f"Psqlpy SQL syntax error: {e}"
286
- raise SQLParsingError(msg) from e
287
- msg = f"Psqlpy error: {e}"
288
- raise SQLSpecError(msg) from e
289
- if issubclass(exc_type, Exception):
290
- e = exc_val
291
- error_msg = str(e).lower()
292
- if "parse" in error_msg or "syntax" in error_msg:
293
- msg = f"SQL parsing failed: {e}"
294
- raise SQLParsingError(msg) from e
295
- msg = f"Unexpected async database operation error: {e}"
296
- raise SQLSpecError(msg) from e
125
+ def _map_postgres_exception(self, e: Any) -> None:
126
+ """Map PostgreSQL exception to SQLSpec exception.
127
+
128
+ psqlpy does not expose SQLSTATE codes directly, so we use message-based
129
+ detection to map exceptions.
130
+
131
+ Args:
132
+ e: psqlpy exception instance
133
+
134
+ Raises:
135
+ Specific SQLSpec exception based on error message patterns
136
+ """
137
+ error_msg = str(e).lower()
138
+
139
+ if "unique" in error_msg or "duplicate key" in error_msg:
140
+ self._raise_unique_violation(e, None)
141
+ elif "foreign key" in error_msg or "violates foreign key" in error_msg:
142
+ self._raise_foreign_key_violation(e, None)
143
+ elif "not null" in error_msg or ("null value" in error_msg and "violates not-null" in error_msg):
144
+ self._raise_not_null_violation(e, None)
145
+ elif "check constraint" in error_msg or "violates check constraint" in error_msg:
146
+ self._raise_check_violation(e, None)
147
+ elif "constraint" in error_msg:
148
+ self._raise_integrity_error(e, None)
149
+ elif "syntax error" in error_msg or "parse" in error_msg:
150
+ self._raise_parsing_error(e, None)
151
+ elif "connection" in error_msg or "could not connect" in error_msg:
152
+ self._raise_connection_error(e, None)
153
+ elif "deadlock" in error_msg or "serialization failure" in error_msg:
154
+ self._raise_transaction_error(e, None)
155
+ else:
156
+ self._raise_generic_error(e, None)
157
+
158
+ def _raise_unique_violation(self, e: Any, code: "str | None") -> None:
159
+ msg = f"PostgreSQL unique constraint violation: {e}"
160
+ raise UniqueViolationError(msg) from e
161
+
162
+ def _raise_foreign_key_violation(self, e: Any, code: "str | None") -> None:
163
+ msg = f"PostgreSQL foreign key constraint violation: {e}"
164
+ raise ForeignKeyViolationError(msg) from e
165
+
166
+ def _raise_not_null_violation(self, e: Any, code: "str | None") -> None:
167
+ msg = f"PostgreSQL not-null constraint violation: {e}"
168
+ raise NotNullViolationError(msg) from e
169
+
170
+ def _raise_check_violation(self, e: Any, code: "str | None") -> None:
171
+ msg = f"PostgreSQL check constraint violation: {e}"
172
+ raise CheckViolationError(msg) from e
173
+
174
+ def _raise_integrity_error(self, e: Any, code: "str | None") -> None:
175
+ msg = f"PostgreSQL integrity constraint violation: {e}"
176
+ raise IntegrityError(msg) from e
177
+
178
+ def _raise_parsing_error(self, e: Any, code: "str | None") -> None:
179
+ msg = f"PostgreSQL SQL syntax error: {e}"
180
+ raise SQLParsingError(msg) from e
181
+
182
+ def _raise_connection_error(self, e: Any, code: "str | None") -> None:
183
+ msg = f"PostgreSQL connection error: {e}"
184
+ raise DatabaseConnectionError(msg) from e
185
+
186
+ def _raise_transaction_error(self, e: Any, code: "str | None") -> None:
187
+ msg = f"PostgreSQL transaction error: {e}"
188
+ raise TransactionError(msg) from e
189
+
190
+ def _raise_data_error(self, e: Any, code: "str | None") -> None:
191
+ msg = f"PostgreSQL data error: {e}"
192
+ raise DataError(msg) from e
193
+
194
+ def _raise_operational_error(self, e: Any, code: "str | None") -> None:
195
+ msg = f"PostgreSQL operational error: {e}"
196
+ raise OperationalError(msg) from e
197
+
198
+ def _raise_generic_error(self, e: Any, code: "str | None") -> None:
199
+ msg = f"PostgreSQL database error: {e}"
200
+ raise SQLSpecError(msg) from e
297
201
 
298
202
 
299
203
  class PsqlpyDriver(AsyncDriverAdapterBase):
@@ -303,14 +207,14 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
303
207
  and transaction management.
304
208
  """
305
209
 
306
- __slots__ = ()
210
+ __slots__ = ("_data_dictionary",)
307
211
  dialect = "postgres"
308
212
 
309
213
  def __init__(
310
214
  self,
311
215
  connection: "PsqlpyConnection",
312
- statement_config: "Optional[StatementConfig]" = None,
313
- driver_features: "Optional[dict[str, Any]]" = None,
216
+ statement_config: "StatementConfig | None" = None,
217
+ driver_features: "dict[str, Any] | None" = None,
314
218
  ) -> None:
315
219
  if statement_config is None:
316
220
  cache_config = get_cache_config()
@@ -322,6 +226,7 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
322
226
  )
323
227
 
324
228
  super().__init__(connection=connection, statement_config=statement_config, driver_features=driver_features)
229
+ self._data_dictionary: AsyncDataDictionaryBase | None = None
325
230
 
326
231
  def with_cursor(self, connection: "PsqlpyConnection") -> "PsqlpyCursor":
327
232
  """Create context manager for psqlpy cursor.
@@ -342,7 +247,7 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
342
247
  """
343
248
  return PsqlpyExceptionHandler()
344
249
 
345
- async def _try_special_handling(self, cursor: "PsqlpyConnection", statement: SQL) -> "Optional[SQLResult]":
250
+ async def _try_special_handling(self, cursor: "PsqlpyConnection", statement: SQL) -> "SQLResult | None":
346
251
  """Hook for psqlpy-specific special operations.
347
252
 
348
253
  Args:
@@ -404,10 +309,9 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
404
309
  formatted_parameters = []
405
310
  for param_set in prepared_parameters:
406
311
  if isinstance(param_set, (list, tuple)):
407
- converted_params = [_convert_psqlpy_parameters(param) for param in param_set]
408
- formatted_parameters.append(converted_params)
312
+ formatted_parameters.append(list(param_set))
409
313
  else:
410
- formatted_parameters.append([_convert_psqlpy_parameters(param_set)])
314
+ formatted_parameters.append([param_set])
411
315
 
412
316
  await cursor.execute_many(sql, formatted_parameters)
413
317
 
@@ -427,9 +331,6 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
427
331
  """
428
332
  sql, prepared_parameters = self._get_compiled_sql(statement, self.statement_config)
429
333
 
430
- if prepared_parameters:
431
- prepared_parameters = [_convert_psqlpy_parameters(param) for param in prepared_parameters]
432
-
433
334
  if statement.returns_rows():
434
335
  query_result = await cursor.fetch(sql, prepared_parameters or [])
435
336
  dict_rows: list[dict[str, Any]] = query_result.result() if query_result else []
@@ -511,3 +412,16 @@ class PsqlpyDriver(AsyncDriverAdapterBase):
511
412
  except psqlpy.exceptions.DatabaseError as e:
512
413
  msg = f"Failed to commit psqlpy transaction: {e}"
513
414
  raise SQLSpecError(msg) from e
415
+
416
+ @property
417
+ def data_dictionary(self) -> "AsyncDataDictionaryBase":
418
+ """Get the data dictionary for this driver.
419
+
420
+ Returns:
421
+ Data dictionary instance for metadata queries
422
+ """
423
+ if self._data_dictionary is None:
424
+ from sqlspec.adapters.psqlpy.data_dictionary import PsqlpyAsyncDataDictionary
425
+
426
+ self._data_dictionary = PsqlpyAsyncDataDictionary()
427
+ return self._data_dictionary
@@ -0,0 +1,5 @@
1
+ """Litestar integration for psqlpy adapter."""
2
+
3
+ from sqlspec.adapters.psqlpy.litestar.store import PsqlpyStore
4
+
5
+ __all__ = ("PsqlpyStore",)