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.
- sqlspec/__init__.py +7 -15
- sqlspec/_serialization.py +256 -24
- sqlspec/_typing.py +71 -52
- sqlspec/adapters/adbc/_types.py +1 -1
- sqlspec/adapters/adbc/adk/__init__.py +5 -0
- sqlspec/adapters/adbc/adk/store.py +870 -0
- sqlspec/adapters/adbc/config.py +69 -12
- sqlspec/adapters/adbc/data_dictionary.py +340 -0
- sqlspec/adapters/adbc/driver.py +266 -58
- sqlspec/adapters/adbc/litestar/__init__.py +5 -0
- sqlspec/adapters/adbc/litestar/store.py +504 -0
- sqlspec/adapters/adbc/type_converter.py +153 -0
- sqlspec/adapters/aiosqlite/_types.py +1 -1
- sqlspec/adapters/aiosqlite/adk/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/adk/store.py +527 -0
- sqlspec/adapters/aiosqlite/config.py +88 -15
- sqlspec/adapters/aiosqlite/data_dictionary.py +149 -0
- sqlspec/adapters/aiosqlite/driver.py +143 -40
- sqlspec/adapters/aiosqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/aiosqlite/litestar/store.py +281 -0
- sqlspec/adapters/aiosqlite/pool.py +7 -7
- sqlspec/adapters/asyncmy/__init__.py +7 -1
- sqlspec/adapters/asyncmy/_types.py +2 -2
- sqlspec/adapters/asyncmy/adk/__init__.py +5 -0
- sqlspec/adapters/asyncmy/adk/store.py +493 -0
- sqlspec/adapters/asyncmy/config.py +68 -23
- sqlspec/adapters/asyncmy/data_dictionary.py +161 -0
- sqlspec/adapters/asyncmy/driver.py +313 -58
- sqlspec/adapters/asyncmy/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncmy/litestar/store.py +296 -0
- sqlspec/adapters/asyncpg/__init__.py +2 -1
- sqlspec/adapters/asyncpg/_type_handlers.py +71 -0
- sqlspec/adapters/asyncpg/_types.py +11 -7
- sqlspec/adapters/asyncpg/adk/__init__.py +5 -0
- sqlspec/adapters/asyncpg/adk/store.py +450 -0
- sqlspec/adapters/asyncpg/config.py +59 -35
- sqlspec/adapters/asyncpg/data_dictionary.py +173 -0
- sqlspec/adapters/asyncpg/driver.py +170 -25
- sqlspec/adapters/asyncpg/litestar/__init__.py +5 -0
- sqlspec/adapters/asyncpg/litestar/store.py +253 -0
- sqlspec/adapters/bigquery/_types.py +1 -1
- sqlspec/adapters/bigquery/adk/__init__.py +5 -0
- sqlspec/adapters/bigquery/adk/store.py +576 -0
- sqlspec/adapters/bigquery/config.py +27 -10
- sqlspec/adapters/bigquery/data_dictionary.py +149 -0
- sqlspec/adapters/bigquery/driver.py +368 -142
- sqlspec/adapters/bigquery/litestar/__init__.py +5 -0
- sqlspec/adapters/bigquery/litestar/store.py +327 -0
- sqlspec/adapters/bigquery/type_converter.py +125 -0
- sqlspec/adapters/duckdb/_types.py +1 -1
- sqlspec/adapters/duckdb/adk/__init__.py +14 -0
- sqlspec/adapters/duckdb/adk/store.py +553 -0
- sqlspec/adapters/duckdb/config.py +80 -20
- sqlspec/adapters/duckdb/data_dictionary.py +163 -0
- sqlspec/adapters/duckdb/driver.py +167 -45
- sqlspec/adapters/duckdb/litestar/__init__.py +5 -0
- sqlspec/adapters/duckdb/litestar/store.py +332 -0
- sqlspec/adapters/duckdb/pool.py +4 -4
- sqlspec/adapters/duckdb/type_converter.py +133 -0
- sqlspec/adapters/oracledb/_numpy_handlers.py +133 -0
- sqlspec/adapters/oracledb/_types.py +20 -2
- sqlspec/adapters/oracledb/adk/__init__.py +5 -0
- sqlspec/adapters/oracledb/adk/store.py +1745 -0
- sqlspec/adapters/oracledb/config.py +122 -32
- sqlspec/adapters/oracledb/data_dictionary.py +509 -0
- sqlspec/adapters/oracledb/driver.py +353 -91
- sqlspec/adapters/oracledb/litestar/__init__.py +5 -0
- sqlspec/adapters/oracledb/litestar/store.py +767 -0
- sqlspec/adapters/oracledb/migrations.py +348 -73
- sqlspec/adapters/oracledb/type_converter.py +207 -0
- sqlspec/adapters/psqlpy/_type_handlers.py +44 -0
- sqlspec/adapters/psqlpy/_types.py +2 -1
- sqlspec/adapters/psqlpy/adk/__init__.py +5 -0
- sqlspec/adapters/psqlpy/adk/store.py +482 -0
- sqlspec/adapters/psqlpy/config.py +46 -17
- sqlspec/adapters/psqlpy/data_dictionary.py +172 -0
- sqlspec/adapters/psqlpy/driver.py +123 -209
- sqlspec/adapters/psqlpy/litestar/__init__.py +5 -0
- sqlspec/adapters/psqlpy/litestar/store.py +272 -0
- sqlspec/adapters/psqlpy/type_converter.py +102 -0
- sqlspec/adapters/psycopg/_type_handlers.py +80 -0
- sqlspec/adapters/psycopg/_types.py +2 -1
- sqlspec/adapters/psycopg/adk/__init__.py +5 -0
- sqlspec/adapters/psycopg/adk/store.py +944 -0
- sqlspec/adapters/psycopg/config.py +69 -35
- sqlspec/adapters/psycopg/data_dictionary.py +331 -0
- sqlspec/adapters/psycopg/driver.py +238 -81
- sqlspec/adapters/psycopg/litestar/__init__.py +5 -0
- sqlspec/adapters/psycopg/litestar/store.py +554 -0
- sqlspec/adapters/sqlite/__init__.py +2 -1
- sqlspec/adapters/sqlite/_type_handlers.py +86 -0
- sqlspec/adapters/sqlite/_types.py +1 -1
- sqlspec/adapters/sqlite/adk/__init__.py +5 -0
- sqlspec/adapters/sqlite/adk/store.py +572 -0
- sqlspec/adapters/sqlite/config.py +87 -15
- sqlspec/adapters/sqlite/data_dictionary.py +149 -0
- sqlspec/adapters/sqlite/driver.py +137 -54
- sqlspec/adapters/sqlite/litestar/__init__.py +5 -0
- sqlspec/adapters/sqlite/litestar/store.py +318 -0
- sqlspec/adapters/sqlite/pool.py +18 -9
- sqlspec/base.py +45 -26
- sqlspec/builder/__init__.py +73 -4
- sqlspec/builder/_base.py +162 -89
- sqlspec/builder/_column.py +62 -29
- sqlspec/builder/_ddl.py +180 -121
- sqlspec/builder/_delete.py +5 -4
- sqlspec/builder/_dml.py +388 -0
- sqlspec/{_sql.py → builder/_factory.py} +53 -94
- sqlspec/builder/_insert.py +32 -131
- sqlspec/builder/_join.py +375 -0
- sqlspec/builder/_merge.py +446 -11
- sqlspec/builder/_parsing_utils.py +111 -17
- sqlspec/builder/_select.py +1457 -24
- sqlspec/builder/_update.py +11 -42
- sqlspec/cli.py +307 -194
- sqlspec/config.py +252 -67
- sqlspec/core/__init__.py +5 -4
- sqlspec/core/cache.py +17 -17
- sqlspec/core/compiler.py +62 -9
- sqlspec/core/filters.py +37 -37
- sqlspec/core/hashing.py +9 -9
- sqlspec/core/parameters.py +83 -48
- sqlspec/core/result.py +102 -46
- sqlspec/core/splitter.py +16 -17
- sqlspec/core/statement.py +36 -30
- sqlspec/core/type_conversion.py +235 -0
- sqlspec/driver/__init__.py +7 -6
- sqlspec/driver/_async.py +188 -151
- sqlspec/driver/_common.py +285 -80
- sqlspec/driver/_sync.py +188 -152
- sqlspec/driver/mixins/_result_tools.py +20 -236
- sqlspec/driver/mixins/_sql_translator.py +4 -4
- sqlspec/exceptions.py +75 -7
- sqlspec/extensions/adk/__init__.py +53 -0
- sqlspec/extensions/adk/_types.py +51 -0
- sqlspec/extensions/adk/converters.py +172 -0
- sqlspec/extensions/adk/migrations/0001_create_adk_tables.py +144 -0
- sqlspec/extensions/adk/migrations/__init__.py +0 -0
- sqlspec/extensions/adk/service.py +181 -0
- sqlspec/extensions/adk/store.py +536 -0
- sqlspec/extensions/aiosql/adapter.py +73 -53
- sqlspec/extensions/litestar/__init__.py +21 -4
- sqlspec/extensions/litestar/cli.py +54 -10
- sqlspec/extensions/litestar/config.py +59 -266
- sqlspec/extensions/litestar/handlers.py +46 -17
- sqlspec/extensions/litestar/migrations/0001_create_session_table.py +137 -0
- sqlspec/extensions/litestar/migrations/__init__.py +3 -0
- sqlspec/extensions/litestar/plugin.py +324 -223
- sqlspec/extensions/litestar/providers.py +25 -25
- sqlspec/extensions/litestar/store.py +265 -0
- sqlspec/loader.py +30 -49
- sqlspec/migrations/__init__.py +4 -3
- sqlspec/migrations/base.py +302 -39
- sqlspec/migrations/commands.py +611 -144
- sqlspec/migrations/context.py +142 -0
- sqlspec/migrations/fix.py +199 -0
- sqlspec/migrations/loaders.py +68 -23
- sqlspec/migrations/runner.py +543 -107
- sqlspec/migrations/tracker.py +237 -21
- sqlspec/migrations/utils.py +51 -3
- sqlspec/migrations/validation.py +177 -0
- sqlspec/protocols.py +66 -36
- sqlspec/storage/_utils.py +98 -0
- sqlspec/storage/backends/fsspec.py +134 -106
- sqlspec/storage/backends/local.py +78 -51
- sqlspec/storage/backends/obstore.py +278 -162
- sqlspec/storage/registry.py +75 -39
- sqlspec/typing.py +16 -84
- sqlspec/utils/config_resolver.py +153 -0
- sqlspec/utils/correlation.py +4 -5
- sqlspec/utils/data_transformation.py +3 -2
- sqlspec/utils/deprecation.py +9 -8
- sqlspec/utils/fixtures.py +4 -4
- sqlspec/utils/logging.py +46 -6
- sqlspec/utils/module_loader.py +2 -2
- sqlspec/utils/schema.py +288 -0
- sqlspec/utils/serializers.py +50 -2
- sqlspec/utils/sync_tools.py +21 -17
- sqlspec/utils/text.py +1 -2
- sqlspec/utils/type_guards.py +111 -20
- sqlspec/utils/version.py +433 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/METADATA +40 -21
- sqlspec-0.27.0.dist-info/RECORD +207 -0
- sqlspec/builder/mixins/__init__.py +0 -55
- sqlspec/builder/mixins/_cte_and_set_ops.py +0 -254
- sqlspec/builder/mixins/_delete_operations.py +0 -50
- sqlspec/builder/mixins/_insert_operations.py +0 -282
- sqlspec/builder/mixins/_join_operations.py +0 -389
- sqlspec/builder/mixins/_merge_operations.py +0 -592
- sqlspec/builder/mixins/_order_limit_operations.py +0 -152
- sqlspec/builder/mixins/_pivot_operations.py +0 -157
- sqlspec/builder/mixins/_select_operations.py +0 -936
- sqlspec/builder/mixins/_update_operations.py +0 -218
- sqlspec/builder/mixins/_where_clause.py +0 -1304
- sqlspec-0.25.0.dist-info/RECORD +0 -139
- sqlspec-0.25.0.dist-info/licenses/NOTICE +0 -29
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/WHEEL +0 -0
- {sqlspec-0.25.0.dist-info → sqlspec-0.27.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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,
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
e
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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: "
|
|
313
|
-
driver_features: "
|
|
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) -> "
|
|
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
|
-
|
|
408
|
-
formatted_parameters.append(converted_params)
|
|
312
|
+
formatted_parameters.append(list(param_set))
|
|
409
313
|
else:
|
|
410
|
-
formatted_parameters.append([
|
|
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
|