.. changelog::
:version: 1.1.5
+ .. change:: 3859
+ :tags: bug, sql
+ :tickets: 3859
+
+ Fixed issue in :class:`.Variant` where the "right hand coercion" logic,
+ inherited from :class:`.TypeDecorator`, would
+ coerce the right-hand side into the :class:`.Variant` itself, rather than
+ what the default type for the :class:`.Variant` would do. In the
+ case of :class:`.Variant`, we want the type to act mostly like the base
+ type so the default logic of :class:`.TypeDecorator` is now overridden
+ to fall back to the underlying wrapped type's logic. Is mostly relevant
+ for JSON at the moment.
+
.. change:: 3856
:tags: bug, orm
:tickets: 3856
Index operations return an expression object whose type defaults to
:class:`.JSON` by default, so that further JSON-oriented instructions
- may be called upon the result type.
+ may be called upon the result type. Note that there are backend-specific
+ idiosyncracies here, including that the Postgresql database does not generally
+ compare a "json" to a "json" structure without type casts. These idiosyncracies
+ can be accommodated in a backend-neutral way by by making explicit use
+ of the :func:`.cast` and :func:`.type_coerce` constructs.
+ Comparison of specific index elements of a :class:`.JSON` object
+ to other objects work best if the **left hand side is CAST to a string**
+ and the **right hand side is rendered as a json string**; a future SQLAlchemy
+ feature such as a generic "astext" modifier may simplify this at some point:
+
+ * **Compare an element of a JSON structure to a string**::
+
+ from sqlalchemy import cast, type_coerce
+ from sqlalchemy import String, JSON
+
+ cast(
+ data_table.c.data['some_key'], String
+ ) == '"some_value"'
+
+ cast(
+ data_table.c.data['some_key'], String
+ ) == type_coerce("some_value", JSON)
+
+ * **Compare an element of a JSON structure to an integer**::
+
+ from sqlalchemy import cast, type_coerce
+ from sqlalchemy import String, JSON
+
+ cast(data_table.c.data['some_key'], String) == '55'
+
+ cast(
+ data_table.c.data['some_key'], String
+ ) == type_coerce(55, JSON)
+
+ * **Compare an element of a JSON structure to some other JSON structure** - note
+ that Python dictionaries are typically not ordered so care should be taken
+ here to assert that the JSON structures are identical::
+
+ from sqlalchemy import cast, type_coerce
+ from sqlalchemy import String, JSON
+ import json
+
+ cast(
+ data_table.c.data['some_key'], String
+ ) == json.dumps({"foo": "bar"})
+
+ cast(
+ data_table.c.data['some_key'], String
+ ) == type_coerce({"foo": "bar"}, JSON)
The :class:`.JSON` type, when used with the SQLAlchemy ORM, does not
detect in-place mutations to the structure. In order to detect these, the
self.impl = base
self.mapping = mapping
+ def coerce_compared_value(self, operator, value):
+ return self.impl.coerce_compared_value(operator, value)
+
def load_dialect_impl(self, dialect):
if dialect.name in self.mapping:
return self.mapping[dialect.name]
from ..config import requirements
from sqlalchemy import Integer, Unicode, UnicodeText, select
from sqlalchemy import Date, DateTime, Time, MetaData, String, \
- Text, Numeric, Float, literal, Boolean, cast, null, JSON, and_
+ Text, Numeric, Float, literal, Boolean, cast, null, JSON, and_, type_coerce
from ..schema import Table, Column
from ... import testing
import decimal
}
}
+ data6 = {
+ "a": 5,
+ "b": "some value",
+ "c": {"foo": "bar"}
+ }
+
@classmethod
def define_tables(cls, metadata):
Table('data_table', metadata,
{"name": "r2", "data": self.data2},
{"name": "r3", "data": self.data3},
{"name": "r4", "data": self.data4},
- {"name": "r5", "data": self.data5}]
+ {"name": "r5", "data": self.data5},
+ {"name": "r6", "data": self.data6}]
)
- def _test_index_criteria(self, crit, expected):
+ def _test_index_criteria(self, crit, expected, test_literal=True):
self._criteria_fixture()
with config.db.connect() as conn:
stmt = select([self.tables.data_table.c.name]).where(crit)
expected
)
- literal_sql = str(stmt.compile(
- config.db, compile_kwargs={"literal_binds": True}))
+ if test_literal:
+ literal_sql = str(stmt.compile(
+ config.db, compile_kwargs={"literal_binds": True}))
- eq_(conn.scalar(literal_sql), expected)
+ eq_(conn.scalar(literal_sql), expected)
def test_crit_spaces_in_key(self):
name = self.tables.data_table.c.name
"r5"
)
+ def test_crit_against_string_basic(self):
+ name = self.tables.data_table.c.name
+ col = self.tables.data_table.c['data']
+
+ self._test_index_criteria(
+ and_(name == 'r6', cast(col["b"], String) == '"some value"'),
+ "r6"
+ )
+
+ def test_crit_against_string_coerce_type(self):
+ name = self.tables.data_table.c.name
+ col = self.tables.data_table.c['data']
+
+ self._test_index_criteria(
+ and_(name == 'r6',
+ cast(col["b"], String) == type_coerce("some value", JSON)),
+ "r6",
+ test_literal=False
+ )
+
+ def test_crit_against_int_basic(self):
+ name = self.tables.data_table.c.name
+ col = self.tables.data_table.c['data']
+
+ self._test_index_criteria(
+ and_(name == 'r6', cast(col["a"], String) == '5'),
+ "r6"
+ )
+
+ def test_crit_against_int_coerce_type(self):
+ name = self.tables.data_table.c.name
+ col = self.tables.data_table.c['data']
+
+ self._test_index_criteria(
+ and_(name == 'r6', cast(col["a"], String) == type_coerce(5, JSON)),
+ "r6",
+ test_literal=False
+ )
+
def test_unicode_round_trip(self):
s = select([
cast(
tab = table('test', column('bvalue', MyTypeDec))
expr = tab.c.bvalue + 6
+
self.assert_compile(
expr,
"test.bvalue || :bvalue_1",
use_default_dialect=True
)
- assert expr.type.__class__ is MyTypeDec
+ is_(expr.right.type.__class__, MyTypeDec)
+ is_(expr.type.__class__, MyTypeDec)
+
eq_(
testing.db.execute(select([expr.label('foo')])).scalar(),
"BIND_INfooBIND_IN6BIND_OUT"
)
+ def test_variant_righthand_coercion(self):
+ my_json_normal = JSON()
+ my_json_variant = JSON().with_variant(String(), "sqlite")
+
+ tab = table(
+ 'test',
+ column('avalue', my_json_normal),
+ column('bvalue', my_json_variant)
+ )
+ expr = tab.c.avalue['foo'] == 'bar'
+
+ is_(expr.right.type._type_affinity, String)
+
+ expr = tab.c.bvalue['foo'] == 'bar'
+
+ is_(expr.right.type._type_affinity, String)
+
def test_bind_typing(self):
from sqlalchemy.sql import column