SQLAlchemy 2.0.47__cp313-cp313t-win32.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.
- sqlalchemy/__init__.py +283 -0
- sqlalchemy/connectors/__init__.py +18 -0
- sqlalchemy/connectors/aioodbc.py +184 -0
- sqlalchemy/connectors/asyncio.py +429 -0
- sqlalchemy/connectors/pyodbc.py +250 -0
- sqlalchemy/cyextension/__init__.py +6 -0
- sqlalchemy/cyextension/collections.cp313t-win32.pyd +0 -0
- sqlalchemy/cyextension/collections.pyx +409 -0
- sqlalchemy/cyextension/immutabledict.cp313t-win32.pyd +0 -0
- sqlalchemy/cyextension/immutabledict.pxd +8 -0
- sqlalchemy/cyextension/immutabledict.pyx +133 -0
- sqlalchemy/cyextension/processors.cp313t-win32.pyd +0 -0
- sqlalchemy/cyextension/processors.pyx +68 -0
- sqlalchemy/cyextension/resultproxy.cp313t-win32.pyd +0 -0
- sqlalchemy/cyextension/resultproxy.pyx +102 -0
- sqlalchemy/cyextension/util.cp313t-win32.pyd +0 -0
- sqlalchemy/cyextension/util.pyx +90 -0
- sqlalchemy/dialects/__init__.py +62 -0
- sqlalchemy/dialects/_typing.py +30 -0
- sqlalchemy/dialects/mssql/__init__.py +88 -0
- sqlalchemy/dialects/mssql/aioodbc.py +63 -0
- sqlalchemy/dialects/mssql/base.py +4093 -0
- sqlalchemy/dialects/mssql/information_schema.py +285 -0
- sqlalchemy/dialects/mssql/json.py +129 -0
- sqlalchemy/dialects/mssql/provision.py +185 -0
- sqlalchemy/dialects/mssql/pymssql.py +126 -0
- sqlalchemy/dialects/mssql/pyodbc.py +760 -0
- sqlalchemy/dialects/mysql/__init__.py +104 -0
- sqlalchemy/dialects/mysql/aiomysql.py +250 -0
- sqlalchemy/dialects/mysql/asyncmy.py +231 -0
- sqlalchemy/dialects/mysql/base.py +3949 -0
- sqlalchemy/dialects/mysql/cymysql.py +106 -0
- sqlalchemy/dialects/mysql/dml.py +225 -0
- sqlalchemy/dialects/mysql/enumerated.py +282 -0
- sqlalchemy/dialects/mysql/expression.py +146 -0
- sqlalchemy/dialects/mysql/json.py +91 -0
- sqlalchemy/dialects/mysql/mariadb.py +72 -0
- sqlalchemy/dialects/mysql/mariadbconnector.py +322 -0
- sqlalchemy/dialects/mysql/mysqlconnector.py +302 -0
- sqlalchemy/dialects/mysql/mysqldb.py +314 -0
- sqlalchemy/dialects/mysql/provision.py +153 -0
- sqlalchemy/dialects/mysql/pymysql.py +158 -0
- sqlalchemy/dialects/mysql/pyodbc.py +157 -0
- sqlalchemy/dialects/mysql/reflection.py +727 -0
- sqlalchemy/dialects/mysql/reserved_words.py +570 -0
- sqlalchemy/dialects/mysql/types.py +835 -0
- sqlalchemy/dialects/oracle/__init__.py +81 -0
- sqlalchemy/dialects/oracle/base.py +3802 -0
- sqlalchemy/dialects/oracle/cx_oracle.py +1555 -0
- sqlalchemy/dialects/oracle/dictionary.py +507 -0
- sqlalchemy/dialects/oracle/oracledb.py +941 -0
- sqlalchemy/dialects/oracle/provision.py +297 -0
- sqlalchemy/dialects/oracle/types.py +316 -0
- sqlalchemy/dialects/oracle/vector.py +365 -0
- sqlalchemy/dialects/postgresql/__init__.py +167 -0
- sqlalchemy/dialects/postgresql/_psycopg_common.py +189 -0
- sqlalchemy/dialects/postgresql/array.py +519 -0
- sqlalchemy/dialects/postgresql/asyncpg.py +1284 -0
- sqlalchemy/dialects/postgresql/base.py +5378 -0
- sqlalchemy/dialects/postgresql/dml.py +339 -0
- sqlalchemy/dialects/postgresql/ext.py +540 -0
- sqlalchemy/dialects/postgresql/hstore.py +406 -0
- sqlalchemy/dialects/postgresql/json.py +404 -0
- sqlalchemy/dialects/postgresql/named_types.py +524 -0
- sqlalchemy/dialects/postgresql/operators.py +129 -0
- sqlalchemy/dialects/postgresql/pg8000.py +669 -0
- sqlalchemy/dialects/postgresql/pg_catalog.py +326 -0
- sqlalchemy/dialects/postgresql/provision.py +183 -0
- sqlalchemy/dialects/postgresql/psycopg.py +862 -0
- sqlalchemy/dialects/postgresql/psycopg2.py +892 -0
- sqlalchemy/dialects/postgresql/psycopg2cffi.py +61 -0
- sqlalchemy/dialects/postgresql/ranges.py +1031 -0
- sqlalchemy/dialects/postgresql/types.py +313 -0
- sqlalchemy/dialects/sqlite/__init__.py +57 -0
- sqlalchemy/dialects/sqlite/aiosqlite.py +482 -0
- sqlalchemy/dialects/sqlite/base.py +3056 -0
- sqlalchemy/dialects/sqlite/dml.py +263 -0
- sqlalchemy/dialects/sqlite/json.py +92 -0
- sqlalchemy/dialects/sqlite/provision.py +229 -0
- sqlalchemy/dialects/sqlite/pysqlcipher.py +157 -0
- sqlalchemy/dialects/sqlite/pysqlite.py +756 -0
- sqlalchemy/dialects/type_migration_guidelines.txt +145 -0
- sqlalchemy/engine/__init__.py +62 -0
- sqlalchemy/engine/_py_processors.py +136 -0
- sqlalchemy/engine/_py_row.py +128 -0
- sqlalchemy/engine/_py_util.py +74 -0
- sqlalchemy/engine/base.py +3390 -0
- sqlalchemy/engine/characteristics.py +155 -0
- sqlalchemy/engine/create.py +893 -0
- sqlalchemy/engine/cursor.py +2298 -0
- sqlalchemy/engine/default.py +2394 -0
- sqlalchemy/engine/events.py +965 -0
- sqlalchemy/engine/interfaces.py +3471 -0
- sqlalchemy/engine/mock.py +134 -0
- sqlalchemy/engine/processors.py +61 -0
- sqlalchemy/engine/reflection.py +2102 -0
- sqlalchemy/engine/result.py +2399 -0
- sqlalchemy/engine/row.py +400 -0
- sqlalchemy/engine/strategies.py +16 -0
- sqlalchemy/engine/url.py +924 -0
- sqlalchemy/engine/util.py +167 -0
- sqlalchemy/event/__init__.py +26 -0
- sqlalchemy/event/api.py +220 -0
- sqlalchemy/event/attr.py +676 -0
- sqlalchemy/event/base.py +472 -0
- sqlalchemy/event/legacy.py +258 -0
- sqlalchemy/event/registry.py +390 -0
- sqlalchemy/events.py +17 -0
- sqlalchemy/exc.py +832 -0
- sqlalchemy/ext/__init__.py +11 -0
- sqlalchemy/ext/associationproxy.py +2027 -0
- sqlalchemy/ext/asyncio/__init__.py +25 -0
- sqlalchemy/ext/asyncio/base.py +281 -0
- sqlalchemy/ext/asyncio/engine.py +1471 -0
- sqlalchemy/ext/asyncio/exc.py +21 -0
- sqlalchemy/ext/asyncio/result.py +965 -0
- sqlalchemy/ext/asyncio/scoping.py +1599 -0
- sqlalchemy/ext/asyncio/session.py +1947 -0
- sqlalchemy/ext/automap.py +1701 -0
- sqlalchemy/ext/baked.py +570 -0
- sqlalchemy/ext/compiler.py +600 -0
- sqlalchemy/ext/declarative/__init__.py +65 -0
- sqlalchemy/ext/declarative/extensions.py +564 -0
- sqlalchemy/ext/horizontal_shard.py +478 -0
- sqlalchemy/ext/hybrid.py +1535 -0
- sqlalchemy/ext/indexable.py +364 -0
- sqlalchemy/ext/instrumentation.py +450 -0
- sqlalchemy/ext/mutable.py +1085 -0
- sqlalchemy/ext/mypy/__init__.py +6 -0
- sqlalchemy/ext/mypy/apply.py +324 -0
- sqlalchemy/ext/mypy/decl_class.py +515 -0
- sqlalchemy/ext/mypy/infer.py +590 -0
- sqlalchemy/ext/mypy/names.py +335 -0
- sqlalchemy/ext/mypy/plugin.py +303 -0
- sqlalchemy/ext/mypy/util.py +357 -0
- sqlalchemy/ext/orderinglist.py +439 -0
- sqlalchemy/ext/serializer.py +185 -0
- sqlalchemy/future/__init__.py +16 -0
- sqlalchemy/future/engine.py +15 -0
- sqlalchemy/inspection.py +174 -0
- sqlalchemy/log.py +288 -0
- sqlalchemy/orm/__init__.py +171 -0
- sqlalchemy/orm/_orm_constructors.py +2661 -0
- sqlalchemy/orm/_typing.py +179 -0
- sqlalchemy/orm/attributes.py +2845 -0
- sqlalchemy/orm/base.py +971 -0
- sqlalchemy/orm/bulk_persistence.py +2135 -0
- sqlalchemy/orm/clsregistry.py +571 -0
- sqlalchemy/orm/collections.py +1627 -0
- sqlalchemy/orm/context.py +3334 -0
- sqlalchemy/orm/decl_api.py +2004 -0
- sqlalchemy/orm/decl_base.py +2192 -0
- sqlalchemy/orm/dependency.py +1302 -0
- sqlalchemy/orm/descriptor_props.py +1092 -0
- sqlalchemy/orm/dynamic.py +300 -0
- sqlalchemy/orm/evaluator.py +379 -0
- sqlalchemy/orm/events.py +3252 -0
- sqlalchemy/orm/exc.py +237 -0
- sqlalchemy/orm/identity.py +302 -0
- sqlalchemy/orm/instrumentation.py +754 -0
- sqlalchemy/orm/interfaces.py +1496 -0
- sqlalchemy/orm/loading.py +1686 -0
- sqlalchemy/orm/mapped_collection.py +557 -0
- sqlalchemy/orm/mapper.py +4444 -0
- sqlalchemy/orm/path_registry.py +809 -0
- sqlalchemy/orm/persistence.py +1788 -0
- sqlalchemy/orm/properties.py +935 -0
- sqlalchemy/orm/query.py +3459 -0
- sqlalchemy/orm/relationships.py +3508 -0
- sqlalchemy/orm/scoping.py +2148 -0
- sqlalchemy/orm/session.py +5280 -0
- sqlalchemy/orm/state.py +1168 -0
- sqlalchemy/orm/state_changes.py +196 -0
- sqlalchemy/orm/strategies.py +3470 -0
- sqlalchemy/orm/strategy_options.py +2568 -0
- sqlalchemy/orm/sync.py +164 -0
- sqlalchemy/orm/unitofwork.py +796 -0
- sqlalchemy/orm/util.py +2403 -0
- sqlalchemy/orm/writeonly.py +674 -0
- sqlalchemy/pool/__init__.py +44 -0
- sqlalchemy/pool/base.py +1524 -0
- sqlalchemy/pool/events.py +375 -0
- sqlalchemy/pool/impl.py +588 -0
- sqlalchemy/py.typed +0 -0
- sqlalchemy/schema.py +69 -0
- sqlalchemy/sql/__init__.py +145 -0
- sqlalchemy/sql/_dml_constructors.py +132 -0
- sqlalchemy/sql/_elements_constructors.py +1872 -0
- sqlalchemy/sql/_orm_types.py +20 -0
- sqlalchemy/sql/_py_util.py +75 -0
- sqlalchemy/sql/_selectable_constructors.py +763 -0
- sqlalchemy/sql/_typing.py +482 -0
- sqlalchemy/sql/annotation.py +587 -0
- sqlalchemy/sql/base.py +2293 -0
- sqlalchemy/sql/cache_key.py +1057 -0
- sqlalchemy/sql/coercions.py +1404 -0
- sqlalchemy/sql/compiler.py +8081 -0
- sqlalchemy/sql/crud.py +1752 -0
- sqlalchemy/sql/ddl.py +1444 -0
- sqlalchemy/sql/default_comparator.py +551 -0
- sqlalchemy/sql/dml.py +1850 -0
- sqlalchemy/sql/elements.py +5589 -0
- sqlalchemy/sql/events.py +458 -0
- sqlalchemy/sql/expression.py +159 -0
- sqlalchemy/sql/functions.py +2158 -0
- sqlalchemy/sql/lambdas.py +1442 -0
- sqlalchemy/sql/naming.py +209 -0
- sqlalchemy/sql/operators.py +2623 -0
- sqlalchemy/sql/roles.py +323 -0
- sqlalchemy/sql/schema.py +6222 -0
- sqlalchemy/sql/selectable.py +7265 -0
- sqlalchemy/sql/sqltypes.py +3930 -0
- sqlalchemy/sql/traversals.py +1024 -0
- sqlalchemy/sql/type_api.py +2368 -0
- sqlalchemy/sql/util.py +1485 -0
- sqlalchemy/sql/visitors.py +1164 -0
- sqlalchemy/testing/__init__.py +96 -0
- sqlalchemy/testing/assertions.py +994 -0
- sqlalchemy/testing/assertsql.py +520 -0
- sqlalchemy/testing/asyncio.py +135 -0
- sqlalchemy/testing/config.py +434 -0
- sqlalchemy/testing/engines.py +483 -0
- sqlalchemy/testing/entities.py +117 -0
- sqlalchemy/testing/exclusions.py +476 -0
- sqlalchemy/testing/fixtures/__init__.py +28 -0
- sqlalchemy/testing/fixtures/base.py +384 -0
- sqlalchemy/testing/fixtures/mypy.py +332 -0
- sqlalchemy/testing/fixtures/orm.py +227 -0
- sqlalchemy/testing/fixtures/sql.py +482 -0
- sqlalchemy/testing/pickleable.py +155 -0
- sqlalchemy/testing/plugin/__init__.py +6 -0
- sqlalchemy/testing/plugin/bootstrap.py +51 -0
- sqlalchemy/testing/plugin/plugin_base.py +828 -0
- sqlalchemy/testing/plugin/pytestplugin.py +892 -0
- sqlalchemy/testing/profiling.py +329 -0
- sqlalchemy/testing/provision.py +603 -0
- sqlalchemy/testing/requirements.py +1945 -0
- sqlalchemy/testing/schema.py +198 -0
- sqlalchemy/testing/suite/__init__.py +19 -0
- sqlalchemy/testing/suite/test_cte.py +237 -0
- sqlalchemy/testing/suite/test_ddl.py +389 -0
- sqlalchemy/testing/suite/test_deprecations.py +153 -0
- sqlalchemy/testing/suite/test_dialect.py +776 -0
- sqlalchemy/testing/suite/test_insert.py +630 -0
- sqlalchemy/testing/suite/test_reflection.py +3557 -0
- sqlalchemy/testing/suite/test_results.py +504 -0
- sqlalchemy/testing/suite/test_rowcount.py +258 -0
- sqlalchemy/testing/suite/test_select.py +2010 -0
- sqlalchemy/testing/suite/test_sequence.py +317 -0
- sqlalchemy/testing/suite/test_types.py +2147 -0
- sqlalchemy/testing/suite/test_unicode_ddl.py +189 -0
- sqlalchemy/testing/suite/test_update_delete.py +139 -0
- sqlalchemy/testing/util.py +535 -0
- sqlalchemy/testing/warnings.py +52 -0
- sqlalchemy/types.py +74 -0
- sqlalchemy/util/__init__.py +162 -0
- sqlalchemy/util/_collections.py +712 -0
- sqlalchemy/util/_concurrency_py3k.py +288 -0
- sqlalchemy/util/_has_cy.py +40 -0
- sqlalchemy/util/_py_collections.py +541 -0
- sqlalchemy/util/compat.py +421 -0
- sqlalchemy/util/concurrency.py +110 -0
- sqlalchemy/util/deprecations.py +401 -0
- sqlalchemy/util/langhelpers.py +2203 -0
- sqlalchemy/util/preloaded.py +150 -0
- sqlalchemy/util/queue.py +322 -0
- sqlalchemy/util/tool_support.py +201 -0
- sqlalchemy/util/topological.py +120 -0
- sqlalchemy/util/typing.py +734 -0
- sqlalchemy-2.0.47.dist-info/METADATA +243 -0
- sqlalchemy-2.0.47.dist-info/RECORD +274 -0
- sqlalchemy-2.0.47.dist-info/WHEEL +5 -0
- sqlalchemy-2.0.47.dist-info/licenses/LICENSE +19 -0
- sqlalchemy-2.0.47.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# dialects/postgresql/_psycopg_common.py
|
|
2
|
+
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
# mypy: ignore-errors
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import decimal
|
|
11
|
+
|
|
12
|
+
from .array import ARRAY as PGARRAY
|
|
13
|
+
from .base import _DECIMAL_TYPES
|
|
14
|
+
from .base import _FLOAT_TYPES
|
|
15
|
+
from .base import _INT_TYPES
|
|
16
|
+
from .base import PGDialect
|
|
17
|
+
from .base import PGExecutionContext
|
|
18
|
+
from .hstore import HSTORE
|
|
19
|
+
from .pg_catalog import _SpaceVector
|
|
20
|
+
from .pg_catalog import INT2VECTOR
|
|
21
|
+
from .pg_catalog import OIDVECTOR
|
|
22
|
+
from ... import exc
|
|
23
|
+
from ... import types as sqltypes
|
|
24
|
+
from ... import util
|
|
25
|
+
from ...engine import processors
|
|
26
|
+
|
|
27
|
+
_server_side_id = util.counter()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _PsycopgNumeric(sqltypes.Numeric):
|
|
31
|
+
def bind_processor(self, dialect):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def result_processor(self, dialect, coltype):
|
|
35
|
+
if self.asdecimal:
|
|
36
|
+
if coltype in _FLOAT_TYPES:
|
|
37
|
+
return processors.to_decimal_processor_factory(
|
|
38
|
+
decimal.Decimal, self._effective_decimal_return_scale
|
|
39
|
+
)
|
|
40
|
+
elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
|
|
41
|
+
# psycopg returns Decimal natively for 1700
|
|
42
|
+
return None
|
|
43
|
+
else:
|
|
44
|
+
raise exc.InvalidRequestError(
|
|
45
|
+
"Unknown PG numeric type: %d" % coltype
|
|
46
|
+
)
|
|
47
|
+
else:
|
|
48
|
+
if coltype in _FLOAT_TYPES:
|
|
49
|
+
# psycopg returns float natively for 701
|
|
50
|
+
return None
|
|
51
|
+
elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
|
|
52
|
+
return processors.to_float
|
|
53
|
+
else:
|
|
54
|
+
raise exc.InvalidRequestError(
|
|
55
|
+
"Unknown PG numeric type: %d" % coltype
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class _PsycopgFloat(_PsycopgNumeric):
|
|
60
|
+
__visit_name__ = "float"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _PsycopgHStore(HSTORE):
|
|
64
|
+
def bind_processor(self, dialect):
|
|
65
|
+
if dialect._has_native_hstore:
|
|
66
|
+
return None
|
|
67
|
+
else:
|
|
68
|
+
return super().bind_processor(dialect)
|
|
69
|
+
|
|
70
|
+
def result_processor(self, dialect, coltype):
|
|
71
|
+
if dialect._has_native_hstore:
|
|
72
|
+
return None
|
|
73
|
+
else:
|
|
74
|
+
return super().result_processor(dialect, coltype)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _PsycopgARRAY(PGARRAY):
|
|
78
|
+
render_bind_cast = True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _PsycopgINT2VECTOR(_SpaceVector, INT2VECTOR):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class _PsycopgOIDVECTOR(_SpaceVector, OIDVECTOR):
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _PGExecutionContext_common_psycopg(PGExecutionContext):
|
|
90
|
+
def create_server_side_cursor(self):
|
|
91
|
+
# use server-side cursors:
|
|
92
|
+
# psycopg
|
|
93
|
+
# https://www.psycopg.org/psycopg3/docs/advanced/cursors.html#server-side-cursors
|
|
94
|
+
# psycopg2
|
|
95
|
+
# https://www.psycopg.org/docs/usage.html#server-side-cursors
|
|
96
|
+
ident = "c_%s_%s" % (hex(id(self))[2:], hex(_server_side_id())[2:])
|
|
97
|
+
return self._dbapi_connection.cursor(ident)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class _PGDialect_common_psycopg(PGDialect):
|
|
101
|
+
supports_statement_cache = True
|
|
102
|
+
supports_server_side_cursors = True
|
|
103
|
+
|
|
104
|
+
default_paramstyle = "pyformat"
|
|
105
|
+
|
|
106
|
+
_has_native_hstore = True
|
|
107
|
+
|
|
108
|
+
colspecs = util.update_copy(
|
|
109
|
+
PGDialect.colspecs,
|
|
110
|
+
{
|
|
111
|
+
sqltypes.Numeric: _PsycopgNumeric,
|
|
112
|
+
sqltypes.Float: _PsycopgFloat,
|
|
113
|
+
HSTORE: _PsycopgHStore,
|
|
114
|
+
sqltypes.ARRAY: _PsycopgARRAY,
|
|
115
|
+
INT2VECTOR: _PsycopgINT2VECTOR,
|
|
116
|
+
OIDVECTOR: _PsycopgOIDVECTOR,
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def __init__(
|
|
121
|
+
self,
|
|
122
|
+
client_encoding=None,
|
|
123
|
+
use_native_hstore=True,
|
|
124
|
+
**kwargs,
|
|
125
|
+
):
|
|
126
|
+
PGDialect.__init__(self, **kwargs)
|
|
127
|
+
if not use_native_hstore:
|
|
128
|
+
self._has_native_hstore = False
|
|
129
|
+
self.use_native_hstore = use_native_hstore
|
|
130
|
+
self.client_encoding = client_encoding
|
|
131
|
+
|
|
132
|
+
def create_connect_args(self, url):
|
|
133
|
+
opts = url.translate_connect_args(username="user", database="dbname")
|
|
134
|
+
|
|
135
|
+
multihosts, multiports = self._split_multihost_from_url(url)
|
|
136
|
+
|
|
137
|
+
if opts or url.query:
|
|
138
|
+
if not opts:
|
|
139
|
+
opts = {}
|
|
140
|
+
if "port" in opts:
|
|
141
|
+
opts["port"] = int(opts["port"])
|
|
142
|
+
opts.update(url.query)
|
|
143
|
+
|
|
144
|
+
if multihosts:
|
|
145
|
+
opts["host"] = ",".join(multihosts)
|
|
146
|
+
comma_ports = ",".join(str(p) if p else "" for p in multiports)
|
|
147
|
+
if comma_ports:
|
|
148
|
+
opts["port"] = comma_ports
|
|
149
|
+
return ([], opts)
|
|
150
|
+
else:
|
|
151
|
+
# no connection arguments whatsoever; psycopg2.connect()
|
|
152
|
+
# requires that "dsn" be present as a blank string.
|
|
153
|
+
return ([""], opts)
|
|
154
|
+
|
|
155
|
+
def get_isolation_level_values(self, dbapi_connection):
|
|
156
|
+
return (
|
|
157
|
+
"AUTOCOMMIT",
|
|
158
|
+
"READ COMMITTED",
|
|
159
|
+
"READ UNCOMMITTED",
|
|
160
|
+
"REPEATABLE READ",
|
|
161
|
+
"SERIALIZABLE",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def set_deferrable(self, connection, value):
|
|
165
|
+
connection.deferrable = value
|
|
166
|
+
|
|
167
|
+
def get_deferrable(self, connection):
|
|
168
|
+
return connection.deferrable
|
|
169
|
+
|
|
170
|
+
def _do_autocommit(self, connection, value):
|
|
171
|
+
connection.autocommit = value
|
|
172
|
+
|
|
173
|
+
def detect_autocommit_setting(self, dbapi_connection):
|
|
174
|
+
return bool(dbapi_connection.autocommit)
|
|
175
|
+
|
|
176
|
+
def do_ping(self, dbapi_connection):
|
|
177
|
+
before_autocommit = dbapi_connection.autocommit
|
|
178
|
+
|
|
179
|
+
if not before_autocommit:
|
|
180
|
+
dbapi_connection.autocommit = True
|
|
181
|
+
cursor = dbapi_connection.cursor()
|
|
182
|
+
try:
|
|
183
|
+
cursor.execute(self._dialect_specific_select_one)
|
|
184
|
+
finally:
|
|
185
|
+
cursor.close()
|
|
186
|
+
if not before_autocommit and not dbapi_connection.closed:
|
|
187
|
+
dbapi_connection.autocommit = before_autocommit
|
|
188
|
+
|
|
189
|
+
return True
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
# dialects/postgresql/array.py
|
|
2
|
+
# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
|
|
3
|
+
# <see AUTHORS file>
|
|
4
|
+
#
|
|
5
|
+
# This module is part of SQLAlchemy and is released under
|
|
6
|
+
# the MIT License: https://www.opensource.org/licenses/mit-license.php
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any as typing_Any
|
|
13
|
+
from typing import Iterable
|
|
14
|
+
from typing import Optional
|
|
15
|
+
from typing import Sequence
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
from typing import TypeVar
|
|
18
|
+
from typing import Union
|
|
19
|
+
|
|
20
|
+
from .operators import CONTAINED_BY
|
|
21
|
+
from .operators import CONTAINS
|
|
22
|
+
from .operators import OVERLAP
|
|
23
|
+
from ... import types as sqltypes
|
|
24
|
+
from ... import util
|
|
25
|
+
from ...sql import expression
|
|
26
|
+
from ...sql import operators
|
|
27
|
+
from ...sql.visitors import InternalTraversal
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from ...engine.interfaces import Dialect
|
|
31
|
+
from ...sql._typing import _ColumnExpressionArgument
|
|
32
|
+
from ...sql._typing import _TypeEngineArgument
|
|
33
|
+
from ...sql.elements import ColumnElement
|
|
34
|
+
from ...sql.elements import Grouping
|
|
35
|
+
from ...sql.expression import BindParameter
|
|
36
|
+
from ...sql.operators import OperatorType
|
|
37
|
+
from ...sql.selectable import _SelectIterable
|
|
38
|
+
from ...sql.type_api import _BindProcessorType
|
|
39
|
+
from ...sql.type_api import _LiteralProcessorType
|
|
40
|
+
from ...sql.type_api import _ResultProcessorType
|
|
41
|
+
from ...sql.type_api import TypeEngine
|
|
42
|
+
from ...sql.visitors import _TraverseInternalsType
|
|
43
|
+
from ...util.typing import Self
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_T = TypeVar("_T", bound=typing_Any)
|
|
47
|
+
_CT = TypeVar("_CT", bound=typing_Any)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def Any(
|
|
51
|
+
other: typing_Any,
|
|
52
|
+
arrexpr: _ColumnExpressionArgument[_T],
|
|
53
|
+
operator: OperatorType = operators.eq,
|
|
54
|
+
) -> ColumnElement[bool]:
|
|
55
|
+
"""A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.any` method.
|
|
56
|
+
See that method for details.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
return arrexpr.any(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def All(
|
|
64
|
+
other: typing_Any,
|
|
65
|
+
arrexpr: _ColumnExpressionArgument[_T],
|
|
66
|
+
operator: OperatorType = operators.eq,
|
|
67
|
+
) -> ColumnElement[bool]:
|
|
68
|
+
"""A synonym for the ARRAY-level :meth:`.ARRAY.Comparator.all` method.
|
|
69
|
+
See that method for details.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
return arrexpr.all(other, operator) # type: ignore[no-any-return, union-attr] # noqa: E501
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class array(expression.ExpressionClauseList[_T]):
|
|
77
|
+
"""A PostgreSQL ARRAY literal.
|
|
78
|
+
|
|
79
|
+
This is used to produce ARRAY literals in SQL expressions, e.g.::
|
|
80
|
+
|
|
81
|
+
from sqlalchemy.dialects.postgresql import array
|
|
82
|
+
from sqlalchemy.dialects import postgresql
|
|
83
|
+
from sqlalchemy import select, func
|
|
84
|
+
|
|
85
|
+
stmt = select(array([1, 2]) + array([3, 4, 5]))
|
|
86
|
+
|
|
87
|
+
print(stmt.compile(dialect=postgresql.dialect()))
|
|
88
|
+
|
|
89
|
+
Produces the SQL:
|
|
90
|
+
|
|
91
|
+
.. sourcecode:: sql
|
|
92
|
+
|
|
93
|
+
SELECT ARRAY[%(param_1)s, %(param_2)s] ||
|
|
94
|
+
ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1
|
|
95
|
+
|
|
96
|
+
An instance of :class:`.array` will always have the datatype
|
|
97
|
+
:class:`_types.ARRAY`. The "inner" type of the array is inferred from the
|
|
98
|
+
values present, unless the :paramref:`_postgresql.array.type_` keyword
|
|
99
|
+
argument is passed::
|
|
100
|
+
|
|
101
|
+
array(["foo", "bar"], type_=CHAR)
|
|
102
|
+
|
|
103
|
+
When constructing an empty array, the :paramref:`_postgresql.array.type_`
|
|
104
|
+
argument is particularly important as PostgreSQL server typically requires
|
|
105
|
+
a cast to be rendered for the inner type in order to render an empty array.
|
|
106
|
+
SQLAlchemy's compilation for the empty array will produce this cast so
|
|
107
|
+
that::
|
|
108
|
+
|
|
109
|
+
stmt = array([], type_=Integer)
|
|
110
|
+
print(stmt.compile(dialect=postgresql.dialect()))
|
|
111
|
+
|
|
112
|
+
Produces:
|
|
113
|
+
|
|
114
|
+
.. sourcecode:: sql
|
|
115
|
+
|
|
116
|
+
ARRAY[]::INTEGER[]
|
|
117
|
+
|
|
118
|
+
As required by PostgreSQL for empty arrays.
|
|
119
|
+
|
|
120
|
+
.. versionadded:: 2.0.40 added support to render empty PostgreSQL array
|
|
121
|
+
literals with a required cast.
|
|
122
|
+
|
|
123
|
+
Multidimensional arrays are produced by nesting :class:`.array` constructs.
|
|
124
|
+
The dimensionality of the final :class:`_types.ARRAY`
|
|
125
|
+
type is calculated by
|
|
126
|
+
recursively adding the dimensions of the inner :class:`_types.ARRAY`
|
|
127
|
+
type::
|
|
128
|
+
|
|
129
|
+
stmt = select(
|
|
130
|
+
array(
|
|
131
|
+
[array([1, 2]), array([3, 4]), array([column("q"), column("x")])]
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
print(stmt.compile(dialect=postgresql.dialect()))
|
|
135
|
+
|
|
136
|
+
Produces:
|
|
137
|
+
|
|
138
|
+
.. sourcecode:: sql
|
|
139
|
+
|
|
140
|
+
SELECT ARRAY[
|
|
141
|
+
ARRAY[%(param_1)s, %(param_2)s],
|
|
142
|
+
ARRAY[%(param_3)s, %(param_4)s],
|
|
143
|
+
ARRAY[q, x]
|
|
144
|
+
] AS anon_1
|
|
145
|
+
|
|
146
|
+
.. versionadded:: 1.3.6 added support for multidimensional array literals
|
|
147
|
+
|
|
148
|
+
.. seealso::
|
|
149
|
+
|
|
150
|
+
:class:`_postgresql.ARRAY`
|
|
151
|
+
|
|
152
|
+
""" # noqa: E501
|
|
153
|
+
|
|
154
|
+
__visit_name__ = "array"
|
|
155
|
+
|
|
156
|
+
stringify_dialect = "postgresql"
|
|
157
|
+
|
|
158
|
+
_traverse_internals: _TraverseInternalsType = [
|
|
159
|
+
("clauses", InternalTraversal.dp_clauseelement_tuple),
|
|
160
|
+
("type", InternalTraversal.dp_type),
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
clauses: Iterable[_T],
|
|
166
|
+
*,
|
|
167
|
+
type_: Optional[_TypeEngineArgument[_T]] = None,
|
|
168
|
+
**kw: typing_Any,
|
|
169
|
+
):
|
|
170
|
+
r"""Construct an ARRAY literal.
|
|
171
|
+
|
|
172
|
+
:param clauses: iterable, such as a list, containing elements to be
|
|
173
|
+
rendered in the array
|
|
174
|
+
:param type\_: optional type. If omitted, the type is inferred
|
|
175
|
+
from the contents of the array.
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
super().__init__(operators.comma_op, *clauses, **kw)
|
|
179
|
+
|
|
180
|
+
main_type = (
|
|
181
|
+
type_
|
|
182
|
+
if type_ is not None
|
|
183
|
+
else self.clauses[0].type if self.clauses else sqltypes.NULLTYPE
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if isinstance(main_type, ARRAY):
|
|
187
|
+
self.type = ARRAY(
|
|
188
|
+
main_type.item_type,
|
|
189
|
+
dimensions=(
|
|
190
|
+
main_type.dimensions + 1
|
|
191
|
+
if main_type.dimensions is not None
|
|
192
|
+
else 2
|
|
193
|
+
),
|
|
194
|
+
) # type: ignore[assignment]
|
|
195
|
+
else:
|
|
196
|
+
self.type = ARRAY(main_type) # type: ignore[assignment]
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def _select_iterable(self) -> _SelectIterable:
|
|
200
|
+
return (self,)
|
|
201
|
+
|
|
202
|
+
def _bind_param(
|
|
203
|
+
self,
|
|
204
|
+
operator: OperatorType,
|
|
205
|
+
obj: typing_Any,
|
|
206
|
+
type_: Optional[TypeEngine[_T]] = None,
|
|
207
|
+
_assume_scalar: bool = False,
|
|
208
|
+
) -> BindParameter[_T]:
|
|
209
|
+
if _assume_scalar or operator is operators.getitem:
|
|
210
|
+
return expression.BindParameter(
|
|
211
|
+
None,
|
|
212
|
+
obj,
|
|
213
|
+
_compared_to_operator=operator,
|
|
214
|
+
type_=type_,
|
|
215
|
+
_compared_to_type=self.type,
|
|
216
|
+
unique=True,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
else:
|
|
220
|
+
return array(
|
|
221
|
+
[
|
|
222
|
+
self._bind_param(
|
|
223
|
+
operator, o, _assume_scalar=True, type_=type_
|
|
224
|
+
)
|
|
225
|
+
for o in obj
|
|
226
|
+
]
|
|
227
|
+
) # type: ignore[return-value]
|
|
228
|
+
|
|
229
|
+
def self_group(
|
|
230
|
+
self, against: Optional[OperatorType] = None
|
|
231
|
+
) -> Union[Self, Grouping[_T]]:
|
|
232
|
+
if against in (operators.any_op, operators.all_op, operators.getitem):
|
|
233
|
+
return expression.Grouping(self)
|
|
234
|
+
else:
|
|
235
|
+
return self
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ARRAY(sqltypes.ARRAY[_T]):
|
|
239
|
+
"""PostgreSQL ARRAY type.
|
|
240
|
+
|
|
241
|
+
The :class:`_postgresql.ARRAY` type is constructed in the same way
|
|
242
|
+
as the core :class:`_types.ARRAY` type; a member type is required, and a
|
|
243
|
+
number of dimensions is recommended if the type is to be used for more
|
|
244
|
+
than one dimension::
|
|
245
|
+
|
|
246
|
+
from sqlalchemy.dialects import postgresql
|
|
247
|
+
|
|
248
|
+
mytable = Table(
|
|
249
|
+
"mytable",
|
|
250
|
+
metadata,
|
|
251
|
+
Column("data", postgresql.ARRAY(Integer, dimensions=2)),
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
The :class:`_postgresql.ARRAY` type provides all operations defined on the
|
|
255
|
+
core :class:`_types.ARRAY` type, including support for "dimensions",
|
|
256
|
+
indexed access, and simple matching such as
|
|
257
|
+
:meth:`.types.ARRAY.Comparator.any` and
|
|
258
|
+
:meth:`.types.ARRAY.Comparator.all`. :class:`_postgresql.ARRAY`
|
|
259
|
+
class also
|
|
260
|
+
provides PostgreSQL-specific methods for containment operations, including
|
|
261
|
+
:meth:`.postgresql.ARRAY.Comparator.contains`
|
|
262
|
+
:meth:`.postgresql.ARRAY.Comparator.contained_by`, and
|
|
263
|
+
:meth:`.postgresql.ARRAY.Comparator.overlap`, e.g.::
|
|
264
|
+
|
|
265
|
+
mytable.c.data.contains([1, 2])
|
|
266
|
+
|
|
267
|
+
Indexed access is one-based by default, to match that of PostgreSQL;
|
|
268
|
+
for zero-based indexed access, set
|
|
269
|
+
:paramref:`_postgresql.ARRAY.zero_indexes`.
|
|
270
|
+
|
|
271
|
+
Additionally, the :class:`_postgresql.ARRAY`
|
|
272
|
+
type does not work directly in
|
|
273
|
+
conjunction with the :class:`.ENUM` type. For a workaround, see the
|
|
274
|
+
special type at :ref:`postgresql_array_of_enum`.
|
|
275
|
+
|
|
276
|
+
.. container:: topic
|
|
277
|
+
|
|
278
|
+
**Detecting Changes in ARRAY columns when using the ORM**
|
|
279
|
+
|
|
280
|
+
The :class:`_postgresql.ARRAY` type, when used with the SQLAlchemy ORM,
|
|
281
|
+
does not detect in-place mutations to the array. In order to detect
|
|
282
|
+
these, the :mod:`sqlalchemy.ext.mutable` extension must be used, using
|
|
283
|
+
the :class:`.MutableList` class::
|
|
284
|
+
|
|
285
|
+
from sqlalchemy.dialects.postgresql import ARRAY
|
|
286
|
+
from sqlalchemy.ext.mutable import MutableList
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class SomeOrmClass(Base):
|
|
290
|
+
# ...
|
|
291
|
+
|
|
292
|
+
data = Column(MutableList.as_mutable(ARRAY(Integer)))
|
|
293
|
+
|
|
294
|
+
This extension will allow "in-place" changes such to the array
|
|
295
|
+
such as ``.append()`` to produce events which will be detected by the
|
|
296
|
+
unit of work. Note that changes to elements **inside** the array,
|
|
297
|
+
including subarrays that are mutated in place, are **not** detected.
|
|
298
|
+
|
|
299
|
+
Alternatively, assigning a new array value to an ORM element that
|
|
300
|
+
replaces the old one will always trigger a change event.
|
|
301
|
+
|
|
302
|
+
.. seealso::
|
|
303
|
+
|
|
304
|
+
:class:`_types.ARRAY` - base array type
|
|
305
|
+
|
|
306
|
+
:class:`_postgresql.array` - produces a literal array value.
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
def __init__(
|
|
311
|
+
self,
|
|
312
|
+
item_type: _TypeEngineArgument[_T],
|
|
313
|
+
as_tuple: bool = False,
|
|
314
|
+
dimensions: Optional[int] = None,
|
|
315
|
+
zero_indexes: bool = False,
|
|
316
|
+
):
|
|
317
|
+
"""Construct an ARRAY.
|
|
318
|
+
|
|
319
|
+
E.g.::
|
|
320
|
+
|
|
321
|
+
Column("myarray", ARRAY(Integer))
|
|
322
|
+
|
|
323
|
+
Arguments are:
|
|
324
|
+
|
|
325
|
+
:param item_type: The data type of items of this array. Note that
|
|
326
|
+
dimensionality is irrelevant here, so multi-dimensional arrays like
|
|
327
|
+
``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as
|
|
328
|
+
``ARRAY(ARRAY(Integer))`` or such.
|
|
329
|
+
|
|
330
|
+
:param as_tuple=False: Specify whether return results
|
|
331
|
+
should be converted to tuples from lists. DBAPIs such
|
|
332
|
+
as psycopg2 return lists by default. When tuples are
|
|
333
|
+
returned, the results are hashable.
|
|
334
|
+
|
|
335
|
+
:param dimensions: if non-None, the ARRAY will assume a fixed
|
|
336
|
+
number of dimensions. This will cause the DDL emitted for this
|
|
337
|
+
ARRAY to include the exact number of bracket clauses ``[]``,
|
|
338
|
+
and will also optimize the performance of the type overall.
|
|
339
|
+
Note that PG arrays are always implicitly "non-dimensioned",
|
|
340
|
+
meaning they can store any number of dimensions no matter how
|
|
341
|
+
they were declared.
|
|
342
|
+
|
|
343
|
+
:param zero_indexes=False: when True, index values will be converted
|
|
344
|
+
between Python zero-based and PostgreSQL one-based indexes, e.g.
|
|
345
|
+
a value of one will be added to all index values before passing
|
|
346
|
+
to the database.
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
if isinstance(item_type, ARRAY):
|
|
350
|
+
raise ValueError(
|
|
351
|
+
"Do not nest ARRAY types; ARRAY(basetype) "
|
|
352
|
+
"handles multi-dimensional arrays of basetype"
|
|
353
|
+
)
|
|
354
|
+
if isinstance(item_type, type):
|
|
355
|
+
item_type = item_type()
|
|
356
|
+
self.item_type = item_type
|
|
357
|
+
self.as_tuple = as_tuple
|
|
358
|
+
self.dimensions = dimensions
|
|
359
|
+
self.zero_indexes = zero_indexes
|
|
360
|
+
|
|
361
|
+
class Comparator(sqltypes.ARRAY.Comparator[_CT]):
|
|
362
|
+
"""Define comparison operations for :class:`_types.ARRAY`.
|
|
363
|
+
|
|
364
|
+
Note that these operations are in addition to those provided
|
|
365
|
+
by the base :class:`.types.ARRAY.Comparator` class, including
|
|
366
|
+
:meth:`.types.ARRAY.Comparator.any` and
|
|
367
|
+
:meth:`.types.ARRAY.Comparator.all`.
|
|
368
|
+
|
|
369
|
+
"""
|
|
370
|
+
|
|
371
|
+
def contains(
|
|
372
|
+
self, other: typing_Any, **kwargs: typing_Any
|
|
373
|
+
) -> ColumnElement[bool]:
|
|
374
|
+
"""Boolean expression. Test if elements are a superset of the
|
|
375
|
+
elements of the argument array expression.
|
|
376
|
+
|
|
377
|
+
kwargs may be ignored by this operator but are required for API
|
|
378
|
+
conformance.
|
|
379
|
+
"""
|
|
380
|
+
return self.operate(CONTAINS, other, result_type=sqltypes.Boolean)
|
|
381
|
+
|
|
382
|
+
def contained_by(self, other: typing_Any) -> ColumnElement[bool]:
|
|
383
|
+
"""Boolean expression. Test if elements are a proper subset of the
|
|
384
|
+
elements of the argument array expression.
|
|
385
|
+
"""
|
|
386
|
+
return self.operate(
|
|
387
|
+
CONTAINED_BY, other, result_type=sqltypes.Boolean
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
def overlap(self, other: typing_Any) -> ColumnElement[bool]:
|
|
391
|
+
"""Boolean expression. Test if array has elements in common with
|
|
392
|
+
an argument array expression.
|
|
393
|
+
"""
|
|
394
|
+
return self.operate(OVERLAP, other, result_type=sqltypes.Boolean)
|
|
395
|
+
|
|
396
|
+
comparator_factory = Comparator
|
|
397
|
+
|
|
398
|
+
@util.memoized_property
|
|
399
|
+
def _against_native_enum(self) -> bool:
|
|
400
|
+
return (
|
|
401
|
+
isinstance(self.item_type, sqltypes.Enum)
|
|
402
|
+
and self.item_type.native_enum
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
def literal_processor(
|
|
406
|
+
self, dialect: Dialect
|
|
407
|
+
) -> Optional[_LiteralProcessorType[_T]]:
|
|
408
|
+
item_proc = self.item_type.dialect_impl(dialect).literal_processor(
|
|
409
|
+
dialect
|
|
410
|
+
)
|
|
411
|
+
if item_proc is None:
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
def to_str(elements: Iterable[typing_Any]) -> str:
|
|
415
|
+
return f"ARRAY[{', '.join(elements)}]"
|
|
416
|
+
|
|
417
|
+
def process(value: Sequence[typing_Any]) -> str:
|
|
418
|
+
inner = self._apply_item_processor(
|
|
419
|
+
value, item_proc, self.dimensions, to_str
|
|
420
|
+
)
|
|
421
|
+
return inner
|
|
422
|
+
|
|
423
|
+
return process
|
|
424
|
+
|
|
425
|
+
def bind_processor(
|
|
426
|
+
self, dialect: Dialect
|
|
427
|
+
) -> Optional[_BindProcessorType[Sequence[typing_Any]]]:
|
|
428
|
+
item_proc = self.item_type.dialect_impl(dialect).bind_processor(
|
|
429
|
+
dialect
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def process(
|
|
433
|
+
value: Optional[Sequence[typing_Any]],
|
|
434
|
+
) -> Optional[list[typing_Any]]:
|
|
435
|
+
if value is None:
|
|
436
|
+
return value
|
|
437
|
+
else:
|
|
438
|
+
return self._apply_item_processor(
|
|
439
|
+
value, item_proc, self.dimensions, list
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return process
|
|
443
|
+
|
|
444
|
+
def result_processor(
|
|
445
|
+
self, dialect: Dialect, coltype: object
|
|
446
|
+
) -> _ResultProcessorType[Sequence[typing_Any]]:
|
|
447
|
+
item_proc = self.item_type.dialect_impl(dialect).result_processor(
|
|
448
|
+
dialect, coltype
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def process(
|
|
452
|
+
value: Sequence[typing_Any],
|
|
453
|
+
) -> Optional[Sequence[typing_Any]]:
|
|
454
|
+
if value is None:
|
|
455
|
+
return value
|
|
456
|
+
else:
|
|
457
|
+
return self._apply_item_processor(
|
|
458
|
+
value,
|
|
459
|
+
item_proc,
|
|
460
|
+
self.dimensions,
|
|
461
|
+
tuple if self.as_tuple else list,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
if self._against_native_enum:
|
|
465
|
+
super_rp = process
|
|
466
|
+
pattern = re.compile(r"^{(.*)}$")
|
|
467
|
+
|
|
468
|
+
def handle_raw_string(value: str) -> Sequence[Optional[str]]:
|
|
469
|
+
inner = pattern.match(value).group(1) # type: ignore[union-attr] # noqa: E501
|
|
470
|
+
return _split_enum_values(inner)
|
|
471
|
+
|
|
472
|
+
def process(
|
|
473
|
+
value: Sequence[typing_Any],
|
|
474
|
+
) -> Optional[Sequence[typing_Any]]:
|
|
475
|
+
if value is None:
|
|
476
|
+
return value
|
|
477
|
+
# isinstance(value, str) is required to handle
|
|
478
|
+
# the case where a TypeDecorator for and Array of Enum is
|
|
479
|
+
# used like was required in sa < 1.3.17
|
|
480
|
+
return super_rp(
|
|
481
|
+
handle_raw_string(value)
|
|
482
|
+
if isinstance(value, str)
|
|
483
|
+
else value
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return process
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _split_enum_values(array_string: str) -> Sequence[Optional[str]]:
|
|
490
|
+
if '"' not in array_string:
|
|
491
|
+
# no escape char is present so it can just split on the comma
|
|
492
|
+
return [
|
|
493
|
+
r if r != "NULL" else None
|
|
494
|
+
for r in (array_string.split(",") if array_string else [])
|
|
495
|
+
]
|
|
496
|
+
|
|
497
|
+
# handles quoted strings from:
|
|
498
|
+
# r'abc,"quoted","also\\\\quoted", "quoted, comma", "esc \" quot", qpr'
|
|
499
|
+
# returns
|
|
500
|
+
# ['abc', 'quoted', 'also\\quoted', 'quoted, comma', 'esc " quot', 'qpr']
|
|
501
|
+
text = array_string.replace(r"\"", "_$ESC_QUOTE$_")
|
|
502
|
+
text = text.replace(r"\\", "\\")
|
|
503
|
+
result = []
|
|
504
|
+
on_quotes = re.split(r'(")', text)
|
|
505
|
+
in_quotes = False
|
|
506
|
+
for tok in on_quotes:
|
|
507
|
+
if tok == '"':
|
|
508
|
+
in_quotes = not in_quotes
|
|
509
|
+
elif in_quotes:
|
|
510
|
+
result.append(tok.replace("_$ESC_QUOTE$_", '"'))
|
|
511
|
+
else:
|
|
512
|
+
# interpret NULL (without quotes!) as None
|
|
513
|
+
result.extend(
|
|
514
|
+
[
|
|
515
|
+
r if r != "NULL" else None
|
|
516
|
+
for r in re.findall(r"([^\s,]+),?", tok)
|
|
517
|
+
]
|
|
518
|
+
)
|
|
519
|
+
return result
|