Skip to content

Commit aaec1bd

Browse files
CaselITzzzeek
authored andcommitted
Support ARRAY of Enum, JSON or JSONB
Added support for columns or type :class:`.ARRAY` of :class:`.Enum`, :class:`.JSON` or :class:`_postgresql.JSONB` in PostgreSQL. Previously a workaround was required in these use cases. Raise an explicit :class:`.exc.CompileError` when adding a table with a column of type :class:`.ARRAY` of :class:`.Enum` configured with :paramref:`.Enum.native_enum` set to ``False`` when :paramref:`.Enum.create_constraint` is not set to ``False`` Fixes: sqlalchemy#5265 Fixes: sqlalchemy#5266 Change-Id: I83a2d20a599232b7066d0839f3e55ff8b78cd8fc
1 parent 2f617f5 commit aaec1bd

8 files changed

Lines changed: 247 additions & 54 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. change::
2+
:tags: usecase, postgresql
3+
:tickets: 5265
4+
5+
Added support for columns or type :class:`.ARRAY` of :class:`.Enum`,
6+
:class:`.JSON` or :class:`_postgresql.JSONB` in PostgreSQL.
7+
Previously a workaround was required in these use cases.
8+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.. change::
2+
:tags: usecase, postgresql
3+
:tickets: 5266
4+
5+
Raise an explicit :class:`.exc.CompileError` when adding a table with a
6+
column of type :class:`.ARRAY` of :class:`.Enum` configured with
7+
:paramref:`.Enum.native_enum` set to ``False`` when
8+
:paramref:`.Enum.create_constraint` is not set to ``False``

lib/sqlalchemy/dialects/postgresql/array.py

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,14 @@
55
# This module is part of SQLAlchemy and is released under
66
# the MIT License: http://www.opensource.org/licenses/mit-license.php
77

8-
from .base import colspecs
9-
from .base import ischema_names
8+
import re
9+
1010
from ... import types as sqltypes
11+
from ... import util
1112
from ...sql import expression
1213
from ...sql import operators
1314

1415

15-
try:
16-
from uuid import UUID as _python_UUID # noqa
17-
except ImportError:
18-
_python_UUID = None
19-
20-
2116
def Any(other, arrexpr, operator=operators.eq):
2217
"""A synonym for the :meth:`.ARRAY.Comparator.any` method.
2318
@@ -318,6 +313,25 @@ def _proc_array(self, arr, itemproc, dim, collection):
318313
for x in arr
319314
)
320315

316+
@util.memoized_property
317+
def _require_cast(self):
318+
return self._against_native_enum or isinstance(
319+
self.item_type, sqltypes.JSON
320+
)
321+
322+
@util.memoized_property
323+
def _against_native_enum(self):
324+
return (
325+
isinstance(self.item_type, sqltypes.Enum)
326+
and self.item_type.native_enum
327+
)
328+
329+
def bind_expression(self, bindvalue):
330+
if self._require_cast:
331+
return expression.cast(bindvalue, self)
332+
else:
333+
return bindvalue
334+
321335
def bind_processor(self, dialect):
322336
item_proc = self.item_type.dialect_impl(dialect).bind_processor(
323337
dialect
@@ -349,8 +363,23 @@ def process(value):
349363
tuple if self.as_tuple else list,
350364
)
351365

352-
return process
353-
366+
if self._against_native_enum:
367+
super_rp = process
368+
369+
def handle_raw_string(value):
370+
inner = re.match(r"^{(.*)}$", value).group(1)
371+
return inner.split(",") if inner else []
372+
373+
def process(value):
374+
if value is None:
375+
return value
376+
# isinstance(value, util.string_types) is required to handle
377+
# the # case where a TypeDecorator for and Array of Enum is
378+
# used like was required in sa < 1.3.17
379+
return super_rp(
380+
handle_raw_string(value)
381+
if isinstance(value, util.string_types)
382+
else value
383+
)
354384

355-
colspecs[sqltypes.ARRAY] = ARRAY
356-
ischema_names["_array"] = ARRAY
385+
return process

lib/sqlalchemy/dialects/postgresql/base.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -870,9 +870,11 @@ def use_identity(element, compiler, **kw):
870870
^^^^^^^^^^^^^^^^^^^^^
871871
872872
The combination of ENUM and ARRAY is not directly supported by backend
873-
DBAPIs at this time. In order to send and receive an ARRAY of ENUM,
874-
use the following workaround type, which decorates the
875-
:class:`_postgresql.ARRAY` datatype.
873+
DBAPIs at this time. Prior to SQLAlchemy 1.3.17, a special workaround
874+
was needed in order to allow this combination to work, described below.
875+
876+
.. versionchanged:: 1.3.17 The combination of ENUM and ARRAY is now directly
877+
handled by SQLAlchemy's implementation without any workarounds needed.
876878
877879
.. sourcecode:: python
878880
@@ -917,10 +919,15 @@ def process(value):
917919
Using JSON/JSONB with ARRAY
918920
^^^^^^^^^^^^^^^^^^^^^^^^^^^
919921
920-
Similar to using ENUM, for an ARRAY of JSON/JSONB we need to render the
921-
appropriate CAST, however current psycopg2 drivers seem to handle the result
922-
for ARRAY of JSON automatically, so the type is simpler::
922+
Similar to using ENUM, prior to SQLAlchemy 1.3.17, for an ARRAY of JSON/JSONB
923+
we need to render the appropriate CAST. Current psycopg2 drivers accomodate
924+
the result set correctly without any special steps.
925+
926+
.. versionchanged:: 1.3.17 The combination of JSON/JSONB and ARRAY is now
927+
directly handled by SQLAlchemy's implementation without any workarounds
928+
needed.
923929
930+
.. sourcecode:: python
924931
925932
class CastingArray(ARRAY):
926933
def bind_expression(self, bindvalue):
@@ -940,6 +947,10 @@ def bind_expression(self, bindvalue):
940947
import datetime as dt
941948
import re
942949

950+
from . import array as _array
951+
from . import hstore as _hstore
952+
from . import json as _json
953+
from . import ranges as _ranges
943954
from ... import exc
944955
from ... import schema
945956
from ... import sql
@@ -1523,9 +1534,25 @@ def _on_metadata_drop(self, target, bind, checkfirst=False, **kw):
15231534
self.drop(bind=bind, checkfirst=checkfirst)
15241535

15251536

1526-
colspecs = {sqltypes.Interval: INTERVAL, sqltypes.Enum: ENUM}
1537+
colspecs = {
1538+
sqltypes.ARRAY: _array.ARRAY,
1539+
sqltypes.Interval: INTERVAL,
1540+
sqltypes.Enum: ENUM,
1541+
sqltypes.JSON.JSONPathType: _json.JSONPathType,
1542+
sqltypes.JSON: _json.JSON,
1543+
}
15271544

15281545
ischema_names = {
1546+
"_array": _array.ARRAY,
1547+
"hstore": _hstore.HSTORE,
1548+
"json": _json.JSON,
1549+
"jsonb": _json.JSONB,
1550+
"int4range": _ranges.INT4RANGE,
1551+
"int8range": _ranges.INT8RANGE,
1552+
"numrange": _ranges.NUMRANGE,
1553+
"daterange": _ranges.DATERANGE,
1554+
"tsrange": _ranges.TSRANGE,
1555+
"tstzrange": _ranges.TSTZRANGE,
15291556
"integer": INTEGER,
15301557
"bigint": BIGINT,
15311558
"smallint": SMALLINT,
@@ -1917,6 +1944,22 @@ def get_column_specification(self, column, **kwargs):
19171944
colspec += " NOT NULL"
19181945
return colspec
19191946

1947+
def visit_check_constraint(self, constraint):
1948+
if constraint._type_bound:
1949+
typ = list(constraint.columns)[0].type
1950+
if (
1951+
isinstance(typ, sqltypes.ARRAY)
1952+
and isinstance(typ.item_type, sqltypes.Enum)
1953+
and not typ.item_type.native_enum
1954+
):
1955+
raise exc.CompileError(
1956+
"PostgreSQL dialect cannot produce the CHECK constraint "
1957+
"for ARRAY of non-native ENUM; please specify "
1958+
"create_constraint=False on this Enum datatype."
1959+
)
1960+
1961+
return super(PGDDLCompiler, self).visit_check_constraint(constraint)
1962+
19201963
def visit_drop_table_comment(self, drop):
19211964
return "COMMENT ON TABLE %s IS NULL" % self.preparer.format_table(
19221965
drop.element

lib/sqlalchemy/dialects/postgresql/hstore.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import re
99

1010
from .array import ARRAY
11-
from .base import ischema_names
1211
from ... import types as sqltypes
1312
from ... import util
1413
from ...sql import functions as sqlfunc
@@ -268,9 +267,6 @@ def process(value):
268267
return process
269268

270269

271-
ischema_names["hstore"] = HSTORE
272-
273-
274270
class hstore(sqlfunc.GenericFunction):
275271
"""Construct an hstore value within a SQL expression using the
276272
PostgreSQL ``hstore()`` function.

lib/sqlalchemy/dialects/postgresql/json.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
# the MIT License: http://www.opensource.org/licenses/mit-license.php
77
from __future__ import absolute_import
88

9-
from .base import colspecs
10-
from .base import ischema_names
119
from ... import types as sqltypes
1210
from ... import util
1311
from ...sql import operators
@@ -96,9 +94,6 @@ def process(value):
9694
return process
9795

9896

99-
colspecs[sqltypes.JSON.JSONPathType] = JSONPathType
100-
101-
10297
class JSON(sqltypes.JSON):
10398
"""Represent the PostgreSQL JSON type.
10499
@@ -236,10 +231,6 @@ def astext(self):
236231
comparator_factory = Comparator
237232

238233

239-
colspecs[sqltypes.JSON] = JSON
240-
ischema_names["json"] = JSON
241-
242-
243234
class JSONB(JSON):
244235
"""Represent the PostgreSQL JSONB type.
245236
@@ -324,6 +315,3 @@ def contained_by(self, other):
324315
)
325316

326317
comparator_factory = Comparator
327-
328-
329-
ischema_names["jsonb"] = JSONB

lib/sqlalchemy/dialects/postgresql/ranges.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# This module is part of SQLAlchemy and is released under
55
# the MIT License: http://www.opensource.org/licenses/mit-license.php
66

7-
from .base import ischema_names
87
from ... import types as sqltypes
98

109

@@ -108,9 +107,6 @@ class INT4RANGE(RangeOperators, sqltypes.TypeEngine):
108107
__visit_name__ = "INT4RANGE"
109108

110109

111-
ischema_names["int4range"] = INT4RANGE
112-
113-
114110
class INT8RANGE(RangeOperators, sqltypes.TypeEngine):
115111
"""Represent the PostgreSQL INT8RANGE type.
116112
@@ -119,9 +115,6 @@ class INT8RANGE(RangeOperators, sqltypes.TypeEngine):
119115
__visit_name__ = "INT8RANGE"
120116

121117

122-
ischema_names["int8range"] = INT8RANGE
123-
124-
125118
class NUMRANGE(RangeOperators, sqltypes.TypeEngine):
126119
"""Represent the PostgreSQL NUMRANGE type.
127120
@@ -130,9 +123,6 @@ class NUMRANGE(RangeOperators, sqltypes.TypeEngine):
130123
__visit_name__ = "NUMRANGE"
131124

132125

133-
ischema_names["numrange"] = NUMRANGE
134-
135-
136126
class DATERANGE(RangeOperators, sqltypes.TypeEngine):
137127
"""Represent the PostgreSQL DATERANGE type.
138128
@@ -141,9 +131,6 @@ class DATERANGE(RangeOperators, sqltypes.TypeEngine):
141131
__visit_name__ = "DATERANGE"
142132

143133

144-
ischema_names["daterange"] = DATERANGE
145-
146-
147134
class TSRANGE(RangeOperators, sqltypes.TypeEngine):
148135
"""Represent the PostgreSQL TSRANGE type.
149136
@@ -152,15 +139,9 @@ class TSRANGE(RangeOperators, sqltypes.TypeEngine):
152139
__visit_name__ = "TSRANGE"
153140

154141

155-
ischema_names["tsrange"] = TSRANGE
156-
157-
158142
class TSTZRANGE(RangeOperators, sqltypes.TypeEngine):
159143
"""Represent the PostgreSQL TSTZRANGE type.
160144
161145
"""
162146

163147
__visit_name__ = "TSTZRANGE"
164-
165-
166-
ischema_names["tstzrange"] = TSTZRANGE

0 commit comments

Comments
 (0)