diff options
| -rw-r--r-- | doc/build/changelog/unreleased_13/4931.rst | 20 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/firebird/base.py | 26 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/oracle/base.py | 30 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/default.py | 52 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 11 | ||||
| -rw-r--r-- | test/sql/test_quote.py | 49 | ||||
| -rw-r--r-- | test/sql/test_unicode.py | 32 |
7 files changed, 149 insertions, 71 deletions
diff --git a/doc/build/changelog/unreleased_13/4931.rst b/doc/build/changelog/unreleased_13/4931.rst new file mode 100644 index 000000000..164924c4a --- /dev/null +++ b/doc/build/changelog/unreleased_13/4931.rst @@ -0,0 +1,20 @@ +.. change:: + :tags: bug, sql, py3k + :tickets: 4931 + + Changed the ``repr()`` of the :class:`.quoted_name` construct to use + regular string repr() under Python 3, rather than running it through + "backslashreplace" escaping, which can be misleading. + +.. change:: + :tags: bug, oracle, firebird + :tickets: 4931 + + Modified the approach of "name normalization" for the Oracle and Firebird + dialects, which converts from the UPPERCASE-as-case-insensitive convention + of these dialects into lowercase-as-case-insensitive for SQLAlchemy, to not + automatically apply the :class:`.quoted_name` construct to a name that + matches itself under upper or lower case conversion, as is the case for + many non-european characters. All names used within metadata structures + are converted to :class:`.quoted_name` objects in any case; the change + here would only affect the output of some inspection functions. diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index a852c7824..c7c921cb4 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -86,7 +86,6 @@ from sqlalchemy.engine import default from sqlalchemy.engine import reflection from sqlalchemy.sql import compiler from sqlalchemy.sql import expression -from sqlalchemy.sql.elements import quoted_name from sqlalchemy.types import BIGINT from sqlalchemy.types import BLOB from sqlalchemy.types import DATE @@ -659,31 +658,6 @@ class FBDialect(default.DefaultDialect): "implicit_returning", True ) - def normalize_name(self, name): - # Remove trailing spaces: FB uses a CHAR() type, - # that is padded with spaces - name = name and name.rstrip() - if name is None: - return None - elif name.upper() == name and not ( - self.identifier_preparer._requires_quotes - )(name.lower()): - return name.lower() - elif name.lower() == name: - return quoted_name(name, quote=True) - else: - return name - - def denormalize_name(self, name): - if name is None: - return None - elif name.lower() == name and not ( - self.identifier_preparer._requires_quotes - )(name.lower()): - return name.upper() - else: - return name - def has_table(self, connection, table_name, schema=None): """Return ``True`` if the given table exists, ignoring the `schema`.""" diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index 48b90f7e9..4c5a717b9 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -457,7 +457,6 @@ from ...sql import compiler from ...sql import expression from ...sql import util as sql_util from ...sql import visitors -from ...sql.elements import quoted_name from ...types import BLOB from ...types import CHAR from ...types import CLOB @@ -1388,35 +1387,6 @@ class OracleDialect(default.DefaultDialect): ) return cursor.first() is not None - def normalize_name(self, name): - if name is None: - return None - if util.py2k: - if isinstance(name, str): - name = name.decode(self.encoding) - if name.upper() == name and not ( - self.identifier_preparer._requires_quotes - )(name.lower()): - return name.lower() - elif name.lower() == name: - return quoted_name(name, quote=True) - else: - return name - - def denormalize_name(self, name): - if name is None: - return None - elif name.lower() == name and not ( - self.identifier_preparer._requires_quotes - )(name.lower()): - name = name.upper() - if util.py2k: - if not self.supports_unicode_binds: - name = name.encode(self.encoding) - else: - name = unicode(name) # noqa - return name - def _get_default_schema_name(self, connection): return self.normalize_name( connection.execute("SELECT USER FROM DUAL").scalar() diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 79b8622d5..d1253f678 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -29,6 +29,7 @@ from .. import util from ..sql import compiler from ..sql import expression from ..sql import schema +from ..sql.elements import quoted_name AUTOCOMMIT_REGEXP = re.compile( @@ -580,6 +581,57 @@ class DefaultDialect(interfaces.Dialect): # the configured default of this dialect. self.set_isolation_level(dbapi_conn, self.default_isolation_level) + def normalize_name(self, name): + if name is None: + return None + if util.py2k: + if isinstance(name, str): + name = name.decode(self.encoding) + + name_lower = name.lower() + name_upper = name.upper() + + if name_upper == name_lower: + # name has no upper/lower conversion, e.g. non-european characters. + # return unchanged + return name + elif name_upper == name and not ( + self.identifier_preparer._requires_quotes + )(name_lower): + # name is all uppercase and doesn't require quoting; normalize + # to all lower case + return name_lower + elif name_lower == name: + # name is all lower case, which if denormalized means we need to + # force quoting on it + return quoted_name(name, quote=True) + else: + # name is mixed case, means it will be quoted in SQL when used + # later, no normalizes + return name + + def denormalize_name(self, name): + if name is None: + return None + + name_lower = name.lower() + name_upper = name.upper() + + if name_upper == name_lower: + # name has no upper/lower conversion, e.g. non-european characters. + # return unchanged + return name + elif name_lower == name and not ( + self.identifier_preparer._requires_quotes + )(name_lower): + name = name_upper + if util.py2k: + if not self.supports_unicode_binds: + name = name.encode(self.encoding) + else: + name = unicode(name) # noqa + return name + class StrCompileDialect(DefaultDialect): diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 204530ccd..e6f57b8d1 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -4508,10 +4508,13 @@ class quoted_name(util.MemoizedSlots, util.text_type): return util.text_type(self).upper() def __repr__(self): - backslashed = self.encode("ascii", "backslashreplace") - if not util.py2k: - backslashed = backslashed.decode("ascii") - return "'%s'" % backslashed + if util.py2k: + backslashed = self.encode("ascii", "backslashreplace") + if not util.py2k: + backslashed = backslashed.decode("ascii") + return "'%s'" % backslashed + else: + return str.__repr__(self) def _select_iterables(elements): diff --git a/test/sql/test_quote.py b/test/sql/test_quote.py index d7219b12d..aba6a0204 100644 --- a/test/sql/test_quote.py +++ b/test/sql/test_quote.py @@ -1,3 +1,5 @@ +#!coding: utf-8 + from sqlalchemy import CheckConstraint from sqlalchemy import Column from sqlalchemy import column @@ -11,6 +13,7 @@ from sqlalchemy import select from sqlalchemy import sql from sqlalchemy import Table from sqlalchemy import testing +from sqlalchemy import util from sqlalchemy.engine import default from sqlalchemy.sql import compiler from sqlalchemy.sql.elements import _anonymous_label @@ -18,6 +21,7 @@ from sqlalchemy.sql.elements import quoted_name from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures +from sqlalchemy.testing import is_ from sqlalchemy.testing.util import picklers @@ -250,6 +254,14 @@ class QuoteTest(fixtures.TestBase, AssertsCompiledSQL): ') AS "LaLa"', ) + def test_repr_unicode(self): + name = quoted_name(u"姓名", None) + + if util.py2k: + eq_(repr(name), "'\u59d3\u540d'") + else: + eq_(repr(name), repr(u"姓名")) + def test_lower_case_names(self): # Create table with quote defaults metadata = MetaData() @@ -988,3 +1000,40 @@ class QuotedIdentTest(fixtures.TestBase): def _assert_not_quoted(self, value): assert not isinstance(value, quoted_name) + + +class NameNormalizeTest(fixtures.TestBase): + dialect = default.DefaultDialect() + + @testing.combinations( + ("NAME", "name", False), + ("NA ME", "NA ME", False), + ("NaMe", "NaMe", False), + (u"姓名", u"姓名", False), + ("name", "name", True), # an all-lower case name needs quote forced + ) + def test_name_normalize(self, original, normalized, is_quote): + orig_norm = self.dialect.normalize_name(original) + + eq_(orig_norm, normalized) + if is_quote: + is_(orig_norm.quote, True) + else: + assert not isinstance(orig_norm, quoted_name) + + @testing.combinations( + ("name", "NAME", False), + ("NA ME", "NA ME", False), + ("NaMe", "NaMe", False), + (u"姓名", u"姓名", False), + (quoted_name("name", quote=True), "name", True), + ) + def test_name_denormalize(self, original, denormalized, is_quote): + orig_denorm = self.dialect.denormalize_name(original) + + eq_(orig_denorm, denormalized) + + if is_quote: + is_(orig_denorm.quote, True) + else: + assert not isinstance(orig_denorm, quoted_name) diff --git a/test/sql/test_unicode.py b/test/sql/test_unicode.py index 5b51644e6..dd7cad6b2 100644 --- a/test/sql/test_unicode.py +++ b/test/sql/test_unicode.py @@ -6,6 +6,7 @@ from sqlalchemy import ForeignKey from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import testing +from sqlalchemy import util from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures @@ -190,14 +191,23 @@ class UnicodeSchemaTest(fixtures.TestBase): ue("\u6e2c\u8a66"), m, Column(ue("\u6e2c\u8a66_id"), Integer) ) - # I hardly understand what's going on with the backslashes in - # this one on py2k vs. py3k - eq_( - repr(t), - ( - "Table('\\u6e2c\\u8a66', MetaData(bind=None), " - "Column('\\u6e2c\\u8a66_id', Integer(), " - "table=<\u6e2c\u8a66>), " - "schema=None)" - ), - ) + if util.py2k: + eq_( + repr(t), + ( + "Table('\\u6e2c\\u8a66', MetaData(bind=None), " + "Column('\\u6e2c\\u8a66_id', Integer(), " + "table=<\u6e2c\u8a66>), " + "schema=None)" + ), + ) + else: + eq_( + repr(t), + ( + "Table('測試', MetaData(bind=None), " + "Column('測試_id', Integer(), " + "table=<測試>), " + "schema=None)" + ), + ) |
