--- /dev/null
+.. change::
+ :tags: bug, sql
+ :tickets: 9610
+
+ Adjusted the operator precedence for the string concatenation operator to
+ be equal to that of string matching operators, such as
+ :meth:`.ColumnElement.like`, :meth:`.ColumnElement.regexp_match`,
+ :meth:`.ColumnElement.match`, etc., as well as plain ``==`` which has the
+ same precedence as string comparison operators, so that parenthesis will be
+ applied to a string concatenation expression that follows a string match
+ operator. This provides for backends such as PostgreSQL where the "regexp
+ match" operator is apparently of higher precedence than the string
+ concatenation operator.
Entity = self.classes.Entity
self.assert_compile(
Entity.descendants.property.strategy._lazywhere,
- "entity.path LIKE :param_1 || :path_1",
+ "entity.path LIKE (:param_1 || :path_1)",
)
self.assert_compile(
Entity.descendants.property.strategy._rev_lazywhere,
- ":param_1 LIKE entity.path || :path_1",
+ ":param_1 LIKE (entity.path || :path_1)",
)
def test_ancestors_lazyload_clause(self):
# :param_1 LIKE (:param_1 || :path_1)
self.assert_compile(
Entity.anscestors.property.strategy._lazywhere,
- ":param_1 LIKE entity.path || :path_1",
+ ":param_1 LIKE (entity.path || :path_1)",
)
self.assert_compile(
Entity.anscestors.property.strategy._rev_lazywhere,
- "entity.path LIKE :param_1 || :path_1",
+ "entity.path LIKE (:param_1 || :path_1)",
)
def test_descendants_lazyload(self):
self.assert_compile(
sess.query(Entity).join(Entity.descendants.of_type(da)),
"SELECT entity.path AS entity_path FROM entity JOIN entity AS "
- "entity_1 ON entity_1.path LIKE entity.path || :path_1",
+ "entity_1 ON entity_1.path LIKE (entity.path || :path_1)",
)
CompiledSQL(
"SELECT a.id AS a_id, b.id AS b_id FROM a JOIN "
"(SELECT a.id AS "
- "aid, b.id AS id FROM a JOIN b ON a.b_ids LIKE :id_1 || "
- "b.id || :param_1) AS anon_1 ON a.id = anon_1.aid "
+ "aid, b.id AS id FROM a JOIN b ON a.b_ids LIKE (:id_1 || "
+ "b.id || :param_1)) AS anon_1 ON a.id = anon_1.aid "
"JOIN b ON b.id = anon_1.id ORDER BY a.id, b.id"
)
)
CompiledSQL(
"SELECT a.id AS a_id, a.b_ids AS a_b_ids, b_1.id AS b_1_id "
"FROM a LEFT OUTER JOIN ((SELECT a.id AS aid, b.id AS id "
- "FROM a JOIN b ON a.b_ids LIKE :id_1 || b.id || :param_1) "
+ "FROM a JOIN b ON a.b_ids LIKE (:id_1 || b.id || :param_1)) "
"AS anon_1 JOIN b AS b_1 ON b_1.id = anon_1.id) "
"ON a.id = anon_1.aid WHERE a.id = :id_2",
params=[{"id_1": "%", "param_1": "%", "id_2": 2}],
CompiledSQL(
"SELECT a.id AS a_id FROM a WHERE "
"EXISTS (SELECT 1 FROM b, (SELECT a.id AS aid, b.id AS id "
- "FROM a JOIN b ON a.b_ids LIKE :id_1 || b.id || :param_1) "
+ "FROM a JOIN b ON a.b_ids LIKE (:id_1 || b.id || :param_1)) "
"AS anon_1 WHERE a.id = anon_1.aid AND b.id = anon_1.id)",
params=[],
)
CompiledSQL(
"SELECT a_1.id AS a_1_id, b.id AS b_id FROM a AS a_1 JOIN "
"(SELECT a.id AS aid, b.id AS id FROM a JOIN b ON a.b_ids "
- "LIKE :id_1 || b.id || :param_1) AS anon_1 "
+ "LIKE (:id_1 || b.id || :param_1)) AS anon_1 "
"ON a_1.id = anon_1.aid JOIN b ON b.id = anon_1.id "
"WHERE a_1.id IN (__[POSTCOMPILE_primary_keys])",
params=[{"id_1": "%", "param_1": "%", "primary_keys": [2]}],
"<regexp replace>(mytable.myid, :myid_1, :myid_2))",
)
+ @testing.combinations(
+ (lambda c, r: c.regexp_match(r), "<regexp>"),
+ (lambda c, r: c.like(r), "LIKE"),
+ (lambda c, r: ~c.regexp_match(r), "<not regexp>"),
+ (lambda c, r: ~c.match(r), "NOT %s MATCH"),
+ (lambda c, r: c.match(r), "MATCH"),
+ )
+ def test_all_match_precedence_against_concat(self, expr, expected):
+ expr = testing.resolve_lambda(
+ expr, c=self.table.c.myid, r=self.table.c.name + "some text"
+ )
+
+ if "%s" in expected:
+ self.assert_compile(
+ expr,
+ f"{expected % ('mytable.myid', )} (mytable.name || :name_1)",
+ )
+ else:
+ self.assert_compile(
+ expr, f"mytable.myid {expected} (mytable.name || :name_1)"
+ )
+
class ComposedLikeOperatorsTest(fixtures.TestBase, testing.AssertsCompiledSQL):
__dialect__ = "default"
"name=(mytable.name || :name_1) "
"WHERE "
"mytable.myid = hoho(:hoho_1) AND "
- "mytable.name = :param_2 || mytable.name || :param_3",
+ "mytable.name = (:param_2 || mytable.name || :param_3)",
)
def test_unconsumed_names_kwargs(self):
"myid=do_stuff(mytable.myid, :param_1) "
"WHERE "
"mytable.myid = hoho(:hoho_1) AND "
- "mytable.name = :param_2 || mytable.name || :param_3",
+ "mytable.name = (:param_2 || mytable.name || :param_3)",
)
def test_update_ordered_parameters_newstyle_2(self):
"myid=do_stuff(mytable.myid, :param_1) "
"WHERE "
"mytable.myid = hoho(:hoho_1) AND "
- "mytable.name = :param_2 || mytable.name || :param_3",
+ "mytable.name = (:param_2 || mytable.name || :param_3)",
)
def test_update_ordered_parameters_multiple(self):
"name=(mytable.name || :name_1) "
"WHERE "
"mytable.myid = hoho(:hoho_1) AND "
- "mytable.name = :param_2 || mytable.name || :param_3",
+ "mytable.name = (:param_2 || mytable.name || :param_3)",
)
def test_where_empty(self):