.. changelog::
:version: 1.2.0b1
+ .. change:: 3740
+ :tags: bug, sql
+ :tickets: 3740
+
+ The system by which percent signs in SQL statements are "doubled"
+ for escaping purposes has been refined. The "doubling" of percent
+ signs mostly associated with the :obj:`.literal_column` construct
+ as well as operators like :meth:`.ColumnOperators.contains` now
+ occurs based on the stated paramstyle of the DBAPI in use; for
+ percent-sensitive paramstyles as are common with the Postgresql
+ and MySQL drivers the doubling will occur, for others like that
+ of SQLite it will not. This allows more database-agnostic use
+ of the :obj:`.literal_column` construct to be possible.
+
+ .. seealso::
+
+ :ref:`change_3740`
+
.. change:: 3957
:tags: bug, sql
:tickets: 3957
Key Behavioral Changes - ORM
============================
+.. _change_3740:
+
+Percent signs in literal_column() now conditionally escaped
+------------------------------------------------------------
+
+The :obj:`.literal_column` construct now escapes percent sign characters
+conditionally, based on whether or not the DBAPI in use makes use of a
+percent-sign-sensitive paramstyle or not (e.g. 'format' or 'pyformat').
+
+Previously, it was not possible to produce a :obj:`.literal_column`
+construct that stated a single percent sign::
+
+ >>> from sqlalchemy import literal_column
+ >>> print(literal_column('some%symbol'))
+ some%%symbol
+
+The percent sign is now unaffected for dialects that are not set to
+use the 'format' or 'pyformat' paramstyles; dialects such most MySQL
+dialects which do state one of these paramstyles will continue to escape
+as is appropriate::
+
+ >>> from sqlalchemy import literal_column
+ >>> print(literal_column('some%symbol'))
+ some%symbol
+ >>> from sqlalchemy.dialects import mysql
+ >>> print(literal_column('some%symbol').compile(dialect=mysql.dialect()))
+ some%%symbol
+
+As part of this change, the doubling that has been present when using
+operators like :meth:`.ColumnOperators.contains`,
+:meth:`.ColumnOperators.startswith` and :meth:`.ColumnOperators.endswith`
+is also refined to only occur when appropriate.
+
+:ticket:`3740`
+
.. _change_3934:
The after_rollback() Session event now emits before the expiration of objects
>>> conn.execute(stmt).fetchall()
{opensql}SELECT DISTINCT users.name
FROM users, addresses
- WHERE (addresses.email_address LIKE '%%' || users.name || '%%')
+ WHERE (addresses.email_address LIKE '%' || users.name || '%')
()
{stop}[(u'jack',), (u'wendy',)]
UPDATE users SET name=:name FROM addresses
WHERE users.id = addresses.id AND
- addresses.email_address LIKE :email_address_1 || '%%'
+ addresses.email_address LIKE :email_address_1 || '%'
When using MySQL, columns from each table can be assigned to in the
SET clause directly, using the dictionary form passed to :meth:`.Update.values`::
UPDATE users, addresses SET addresses.email_address=%s,
users.name=%s WHERE users.id = addresses.id
- AND addresses.email_address LIKE concat(%s, '%%')
+ AND addresses.email_address LIKE concat(%s, '%')
SQLAlchemy doesn't do anything special when these constructs are used on
a non-supporting database. The ``UPDATE FROM`` syntax generates by default
class MySQLCompiler_mysqldb(MySQLCompiler):
- def visit_mod_binary(self, binary, operator, **kw):
- return self.process(binary.left, **kw) + " %% " + \
- self.process(binary.right, **kw)
-
- def post_process_text(self, text):
- return text.replace('%', '%%')
+ pass
class MySQLIdentifierPreparer_mysqldb(MySQLIdentifierPreparer):
-
- def _escape_identifier(self, value):
- value = value.replace(self.escape_quote, self.escape_to_quote)
- return value.replace("%", "%%")
+ pass
class MySQLDialect_mysqldb(MySQLDialect):
class PGCompiler_psycopg2(PGCompiler):
- def visit_mod_binary(self, binary, operator, **kw):
- return self.process(binary.left, **kw) + " %% " + \
- self.process(binary.right, **kw)
-
- def post_process_text(self, text):
- return text.replace('%', '%%')
+ pass
class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer):
- def _escape_identifier(self, value):
- value = value.replace(self.escape_quote, self.escape_to_quote)
- return value.replace('%', '%%')
+ pass
class PGDialect_psycopg2(PGDialect):
name = self.escape_literal_column(name)
else:
name = self.preparer.quote(name)
-
table = column.table
if table is None or not include_table or not table.named_with_column:
return name
self.preparer.quote(tablename) + \
"." + name
- def escape_literal_column(self, text):
- """provide escaping for the literal_column() construct."""
-
- # TODO: some dialects might need different behavior here
- return text.replace('%', '%%')
-
def visit_fromclause(self, fromclause, **kwargs):
return fromclause.name
return self.dialect.type_compiler.process(typeclause.type, **kw)
def post_process_text(self, text):
+ if self.preparer._double_percents:
+ text = text.replace('%', '%%')
+ return text
+
+ def escape_literal_column(self, text):
+ if self.preparer._double_percents:
+ text = text.replace('%', '%%')
return text
def visit_textclause(self, textclause, **kw):
else:
return self._generate_generic_binary(binary, opstring, **kw)
+ def visit_mod_binary(self, binary, operator, **kw):
+ if self.preparer._double_percents:
+ return self.process(binary.left, **kw) + " %% " + \
+ self.process(binary.right, **kw)
+ else:
+ return self.process(binary.left, **kw) + " % " + \
+ self.process(binary.right, **kw)
+
def visit_custom_op_binary(self, element, operator, **kw):
kw['eager_grouping'] = operator.eager_grouping
return self._generate_generic_binary(
self.escape_to_quote = self.escape_quote * 2
self.omit_schema = omit_schema
self._strings = {}
+ self._double_percents = self.dialect.paramstyle in ('format', 'pyformat')
def _with_schema_translate(self, schema_translate_map):
prep = self.__class__.__new__(self.__class__)
escaping behavior.
"""
- return value.replace(self.escape_quote, self.escape_to_quote)
+ value = value.replace(self.escape_quote, self.escape_to_quote)
+ if self._double_percents:
+ value = value.replace('%', '%%')
+ return value
def _unescape_identifier(self, value):
"""Canonicalize an escaped identifier.
from .. import fixtures, config
from ..config import requirements
from sqlalchemy import exc
-from sqlalchemy import Integer, String
+from sqlalchemy import Integer, String, select, literal_column
from .. import assert_raises
from ..schema import Table, Column
+from .. import provide_metadata
+from .. import eq_
class ExceptionTest(fixtures.TablesTest):
self.tables.manual_pk.insert(),
{'id': 1, 'data': 'd1'}
)
+
+
+class EscapingTest(fixtures.TestBase):
+ @provide_metadata
+ def test_percent_sign_round_trip(self):
+ """test that the DBAPI accommodates for escaped / nonescaped
+ percent signs in a way that matches the compiler
+
+ """
+ m = self.metadata
+ t = Table('t', m, Column('data', String(50)))
+ t.create(config.db)
+ with config.db.begin() as conn:
+ conn.execute(t.insert(), dict(data="some % value"))
+ conn.execute(t.insert(), dict(data="some %% other value"))
+
+ eq_(
+ conn.scalar(
+ select([t.c.data]).where(
+ t.c.data == literal_column("'some % value'"))
+ ),
+ "some % value"
+ )
+
+ eq_(
+ conn.scalar(
+ select([t.c.data]).where(
+ t.c.data == literal_column("'some %% other value'"))
+ ), "some %% other value"
+ )
def test_contains(self):
self.assert_compile(
column('x').contains('y'),
- "x LIKE '%%' || :x_1 || '%%'",
+ "x LIKE '%' || :x_1 || '%'",
checkparams={'x_1': 'y'}
)
def test_contains_escape(self):
self.assert_compile(
column('x').contains('a%b_c', escape='\\'),
- "x LIKE '%%' || :x_1 || '%%' ESCAPE '\\'",
+ "x LIKE '%' || :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_contains_autoescape(self):
self.assert_compile(
column('x').contains('a%b_c', autoescape='\\'),
- "x LIKE '%%' || :x_1 || '%%' ESCAPE '\\'",
+ "x LIKE '%' || :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_contains_literal(self):
self.assert_compile(
column('x').contains(literal_column('y')),
- "x LIKE '%%' || y || '%%'",
+ "x LIKE '%' || y || '%'",
checkparams={}
)
def test_contains_text(self):
self.assert_compile(
column('x').contains(text('y')),
- "x LIKE '%%' || y || '%%'",
+ "x LIKE '%' || y || '%'",
checkparams={}
)
def test_not_contains(self):
self.assert_compile(
~column('x').contains('y'),
- "x NOT LIKE '%%' || :x_1 || '%%'",
+ "x NOT LIKE '%' || :x_1 || '%'",
checkparams={'x_1': 'y'}
)
def test_not_contains_escape(self):
self.assert_compile(
~column('x').contains('a%b_c', escape='\\'),
- "x NOT LIKE '%%' || :x_1 || '%%' ESCAPE '\\'",
+ "x NOT LIKE '%' || :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_not_contains_autoescape(self):
self.assert_compile(
~column('x').contains('a%b_c', autoescape='\\'),
- "x NOT LIKE '%%' || :x_1 || '%%' ESCAPE '\\'",
+ "x NOT LIKE '%' || :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_startswith(self):
self.assert_compile(
column('x').startswith('y'),
- "x LIKE :x_1 || '%%'",
+ "x LIKE :x_1 || '%'",
checkparams={'x_1': 'y'}
)
def test_startswith_escape(self):
self.assert_compile(
column('x').startswith('a%b_c', escape='\\'),
- "x LIKE :x_1 || '%%' ESCAPE '\\'",
+ "x LIKE :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_startswith_autoescape(self):
self.assert_compile(
column('x').startswith('a%b_c', autoescape='\\'),
- "x LIKE :x_1 || '%%' ESCAPE '\\'",
+ "x LIKE :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_not_startswith(self):
self.assert_compile(
~column('x').startswith('y'),
- "x NOT LIKE :x_1 || '%%'",
+ "x NOT LIKE :x_1 || '%'",
checkparams={'x_1': 'y'}
)
def test_not_startswith_escape(self):
self.assert_compile(
~column('x').startswith('a%b_c', escape='\\'),
- "x NOT LIKE :x_1 || '%%' ESCAPE '\\'",
+ "x NOT LIKE :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_not_startswith_autoescape(self):
self.assert_compile(
~column('x').startswith('a%b_c', autoescape='\\'),
- "x NOT LIKE :x_1 || '%%' ESCAPE '\\'",
+ "x NOT LIKE :x_1 || '%' ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_startswith_literal(self):
self.assert_compile(
column('x').startswith(literal_column('y')),
- "x LIKE y || '%%'",
+ "x LIKE y || '%'",
checkparams={}
)
def test_startswith_text(self):
self.assert_compile(
column('x').startswith(text('y')),
- "x LIKE y || '%%'",
+ "x LIKE y || '%'",
checkparams={}
)
def test_endswith(self):
self.assert_compile(
column('x').endswith('y'),
- "x LIKE '%%' || :x_1",
+ "x LIKE '%' || :x_1",
checkparams={'x_1': 'y'}
)
def test_endswith_escape(self):
self.assert_compile(
column('x').endswith('a%b_c', escape='\\'),
- "x LIKE '%%' || :x_1 ESCAPE '\\'",
+ "x LIKE '%' || :x_1 ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_endswith_autoescape(self):
self.assert_compile(
column('x').endswith('a%b_c', autoescape='\\'),
- "x LIKE '%%' || :x_1 ESCAPE '\\'",
+ "x LIKE '%' || :x_1 ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_not_endswith(self):
self.assert_compile(
~column('x').endswith('y'),
- "x NOT LIKE '%%' || :x_1",
+ "x NOT LIKE '%' || :x_1",
checkparams={'x_1': 'y'}
)
def test_not_endswith_escape(self):
self.assert_compile(
~column('x').endswith('a%b_c', escape='\\'),
- "x NOT LIKE '%%' || :x_1 ESCAPE '\\'",
+ "x NOT LIKE '%' || :x_1 ESCAPE '\\'",
checkparams={'x_1': 'a%b_c'}
)
def test_not_endswith_autoescape(self):
self.assert_compile(
~column('x').endswith('a%b_c', autoescape='\\'),
- "x NOT LIKE '%%' || :x_1 ESCAPE '\\'",
+ "x NOT LIKE '%' || :x_1 ESCAPE '\\'",
checkparams={'x_1': 'a\\%b\\_c'}
)
def test_endswith_literal(self):
self.assert_compile(
column('x').endswith(literal_column('y')),
- "x LIKE '%%' || y",
+ "x LIKE '%' || y",
checkparams={}
)
def test_endswith_text(self):
self.assert_compile(
column('x').endswith(text('y')),
- "x LIKE '%%' || y",
+ "x LIKE '%' || y",
checkparams={}
)
-from sqlalchemy import *
+from sqlalchemy import MetaData, Table, Column, Integer, select, \
+ ForeignKey, Index, CheckConstraint, inspect, column
from sqlalchemy import sql, schema
from sqlalchemy.sql import compiler
from sqlalchemy.testing import fixtures, AssertsCompiledSQL, eq_
from sqlalchemy import testing
-from sqlalchemy.sql.elements import (quoted_name,
- _truncated_label,
- _anonymous_label)
+from sqlalchemy.sql.elements import quoted_name, _anonymous_label
from sqlalchemy.testing.util import picklers
+from sqlalchemy.engine import default
class QuoteExecTest(fixtures.TestBase):
"""Test the db-agnostic quoting services of IdentifierPreparer."""
def test_unformat(self):
- prep = compiler.IdentifierPreparer(None)
+ prep = compiler.IdentifierPreparer(default.DefaultDialect())
unformat = prep.unformat_identifiers
def a_eq(have, want):
def _unescape_identifier(self, value):
return value.replace('``', '`')
- prep = Custom(None)
+ prep = Custom(default.DefaultDialect())
unformat = prep.unformat_identifiers
def a_eq(have, want):
checkparams={'y': 6, 'x': 5, 'z': 7}
)
+ def test_escaping_percent_signs(self):
+ stmt = text("select '%' where foo like '%bar%'")
+ self.assert_compile(
+ stmt,
+ "select '%' where foo like '%bar%'",
+ dialect="sqlite"
+ )
+
+ self.assert_compile(
+ stmt,
+ "select '%%' where foo like '%%bar%%'",
+ dialect="mysql"
+ )
class AsFromTest(fixtures.TestBase, AssertsCompiledSQL):
__dialect__ = 'default'