summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2013-08-27 20:43:22 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2013-08-27 20:43:22 -0400
commit031ef0807838842a827135dbace760da7aec215e (patch)
treea677555dd6f39e64da0880035a378ed4323c8e82 /lib/sqlalchemy
parent99732dd29bd69a4a3808bfaa86c8e378d7a5e28b (diff)
downloadsqlalchemy-031ef0807838842a827135dbace760da7aec215e.tar.gz
- A rework to the way that "quoted" identifiers are handled, in that
instead of relying upon various ``quote=True`` flags being passed around, these flags are converted into rich string objects with quoting information included at the point at which they are passed to common schema constructs like :class:`.Table`, :class:`.Column`, etc. This solves the issue of various methods that don't correctly honor the "quote" flag such as :meth:`.Engine.has_table` and related methods. The :class:`.quoted_name` object is a string subclass that can also be used explicitly if needed; the object will hold onto the quoting preferences passed and will also bypass the "name normalization" performed by dialects that standardize on uppercase symbols, such as Oracle, Firebird and DB2. The upshot is that the "uppercase" backends can now work with force-quoted names, such as lowercase-quoted names and new reserved words. [ticket:2812]
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py4
-rw-r--r--lib/sqlalchemy/dialects/mysql/base.py7
-rw-r--r--lib/sqlalchemy/dialects/oracle/cx_oracle.py3
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py8
-rw-r--r--lib/sqlalchemy/engine/base.py11
-rw-r--r--lib/sqlalchemy/engine/default.py2
-rw-r--r--lib/sqlalchemy/engine/reflection.py49
-rw-r--r--lib/sqlalchemy/sql/base.py1
-rw-r--r--lib/sqlalchemy/sql/compiler.py99
-rw-r--r--lib/sqlalchemy/sql/elements.py116
-rw-r--r--lib/sqlalchemy/sql/schema.py127
-rw-r--r--lib/sqlalchemy/sql/selectable.py1
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py15
-rw-r--r--lib/sqlalchemy/sql/util.py1
14 files changed, 317 insertions, 127 deletions
diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py
index 7621f4aab..b1b168035 100644
--- a/lib/sqlalchemy/dialects/mssql/base.py
+++ b/lib/sqlalchemy/dialects/mssql/base.py
@@ -1012,7 +1012,7 @@ class MSDDLCompiler(compiler.DDLCompiler):
for col in index.kwargs["mssql_include"]]
text += " INCLUDE (%s)" \
- % ', '.join([preparer.quote(c.name, c.quote)
+ % ', '.join([preparer.quote(c.name)
for c in inclusions])
return text
@@ -1035,7 +1035,7 @@ class MSIdentifierPreparer(compiler.IdentifierPreparer):
def _escape_identifier(self, value):
return value
- def quote_schema(self, schema, force=True):
+ def quote_schema(self, schema, force=None):
"""Prepare a quoted table and schema name."""
result = '.'.join([self.quote(x, force) for x in schema.split('.')])
return result
diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py
index b36b70774..fd6a47a10 100644
--- a/lib/sqlalchemy/dialects/mysql/base.py
+++ b/lib/sqlalchemy/dialects/mysql/base.py
@@ -1451,7 +1451,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
constraint_string += ", \n\t"
constraint_string += "KEY %s (%s)" % (
self.preparer.quote(
- "idx_autoinc_%s" % auto_inc_column.name, None
+ "idx_autoinc_%s" % auto_inc_column.name
),
self.preparer.format_column(auto_inc_column)
)
@@ -1557,7 +1557,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
if 'mysql_using' in index.kwargs:
using = index.kwargs['mysql_using']
- text += " USING %s" % (preparer.quote(using, index.quote))
+ text += " USING %s" % (preparer.quote(using))
return text
@@ -1566,8 +1566,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
visit_primary_key_constraint(constraint)
if "mysql_using" in constraint.kwargs:
using = constraint.kwargs['mysql_using']
- text += " USING %s" % (
- self.preparer.quote(using, constraint.quote))
+ text += " USING %s" % (self.preparer.quote(using))
return text
def visit_drop_index(self, drop):
diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py
index e013799db..b82d3016f 100644
--- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py
+++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py
@@ -362,7 +362,8 @@ class _OracleRowid(oracle.ROWID):
class OracleCompiler_cx_oracle(OracleCompiler):
- def bindparam_string(self, name, quote=None, **kw):
+ def bindparam_string(self, name, **kw):
+ quote = getattr(name, 'quote', None)
if quote is True or quote is not False and \
self.preparer._bindparam_requires_quotes(name):
quoted_name = '"%s"' % name
diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py
index 6ccf7190e..8938b3193 100644
--- a/lib/sqlalchemy/dialects/postgresql/base.py
+++ b/lib/sqlalchemy/dialects/postgresql/base.py
@@ -1094,7 +1094,7 @@ class PGDDLCompiler(compiler.DDLCompiler):
if 'postgresql_using' in index.kwargs:
using = index.kwargs['postgresql_using']
- text += "USING %s " % preparer.quote(using, index.quote)
+ text += "USING %s " % preparer.quote(using)
ops = index.kwargs.get('postgresql_ops', {})
text += "(%s)" \
@@ -1128,7 +1128,7 @@ class PGDDLCompiler(compiler.DDLCompiler):
elements = []
for c in constraint.columns:
op = constraint.operators[c.name]
- elements.append(self.preparer.quote(c.name, c.quote)+' WITH '+op)
+ elements.append(self.preparer.quote(c.name) + ' WITH '+op)
text += "EXCLUDE USING %s (%s)" % (constraint.using, ', '.join(elements))
if constraint.where is not None:
sqltext = sql_util.expression_as_ddl(constraint.where)
@@ -1250,9 +1250,9 @@ class PGIdentifierPreparer(compiler.IdentifierPreparer):
if not type_.name:
raise exc.CompileError("Postgresql ENUM type requires a name.")
- name = self.quote(type_.name, type_.quote)
+ name = self.quote(type_.name)
if not self.omit_schema and use_schema and type_.schema is not None:
- name = self.quote_schema(type_.schema, type_.quote) + "." + name
+ name = self.quote_schema(type_.schema) + "." + name
return name
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index 735113a26..9a10e829e 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -1672,6 +1672,17 @@ class Engine(Connectable, log.Identified):
return self.dialect.get_table_names(conn, schema)
def has_table(self, table_name, schema=None):
+ """Return True if the given backend has a table of the given name.
+
+ .. seealso::
+
+ :ref:`metadata_reflection_inspector` - detailed schema inspection using
+ the :class:`.Inspector` interface.
+
+ :class:`.quoted_name` - used to pass quoting information along
+ with a schema identifier.
+
+ """
return self.run_callable(self.dialect.has_table, table_name, schema)
def raw_connection(self):
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index 90c7f5993..609375c39 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -27,6 +27,7 @@ AUTOCOMMIT_REGEXP = re.compile(
re.I | re.UNICODE)
+
class DefaultDialect(interfaces.Dialect):
"""Default implementation of Dialect"""
@@ -160,6 +161,7 @@ class DefaultDialect(interfaces.Dialect):
self._encoder = codecs.getencoder(self.encoding)
self._decoder = processors.to_unicode_processor_factory(self.encoding)
+
@util.memoized_property
def _type_memos(self):
return weakref.WeakKeyDictionary()
diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py
index 29ede9579..340af1abb 100644
--- a/lib/sqlalchemy/engine/reflection.py
+++ b/lib/sqlalchemy/engine/reflection.py
@@ -169,7 +169,7 @@ class Inspector(object):
database's default schema is
used, else the named schema is searched. If the database does not
support named schemas, behavior is undefined if ``schema`` is not
- passed as ``None``.
+ passed as ``None``. For special quoting, use :class:`.quoted_name`.
:param order_by: Optional, may be the string "foreign_key" to sort
the result on foreign key dependencies.
@@ -206,6 +206,13 @@ class Inspector(object):
This currently includes some options that apply to MySQL tables.
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
"""
if hasattr(self.dialect, 'get_table_options'):
return self.dialect.get_table_options(
@@ -217,6 +224,8 @@ class Inspector(object):
"""Return all view names in `schema`.
:param schema: Optional, retrieve names from a non-default schema.
+ For special quoting, use :class:`.quoted_name`.
+
"""
return self.dialect.get_view_names(self.bind, schema,
@@ -226,6 +235,8 @@ class Inspector(object):
"""Return definition for `view_name`.
:param schema: Optional, retrieve names from a non-default schema.
+ For special quoting, use :class:`.quoted_name`.
+
"""
return self.dialect.get_view_definition(
@@ -251,6 +262,14 @@ class Inspector(object):
attrs
dict containing optional column attributes
+
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
"""
col_defs = self.dialect.get_columns(self.bind, table_name, schema,
@@ -288,6 +307,13 @@ class Inspector(object):
name
optional name of the primary key constraint.
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
"""
return self.dialect.get_pk_constraint(self.bind, table_name, schema,
info_cache=self.info_cache,
@@ -315,6 +341,13 @@ class Inspector(object):
name
optional name of the foreign key constraint.
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
"""
return self.dialect.get_foreign_keys(self.bind, table_name, schema,
@@ -336,6 +369,13 @@ class Inspector(object):
unique
boolean
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
"""
return self.dialect.get_indexes(self.bind, table_name,
@@ -354,6 +394,13 @@ class Inspector(object):
column_names
list of column names in order
+ :param table_name: string name of the table. For special quoting,
+ use :class:`.quoted_name`.
+
+ :param schema: string schema name; if omitted, uses the default schema
+ of the database connection. For special quoting,
+ use :class:`.quoted_name`.
+
.. versionadded:: 0.9.0
"""
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index 2a3176d04..85c6e730e 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -30,6 +30,7 @@ class Immutable(object):
return self
+
def _from_objects(*elements):
return itertools.chain(*[element._from_objects for element in elements])
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index 5d05cbc29..8e3cf1729 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -24,7 +24,7 @@ To generate user-defined SQL strings, see
import re
from . import schema, sqltypes, operators, functions, \
- util as sql_util, visitors, elements, selectable
+ util as sql_util, visitors, elements, selectable, base
from .. import util, exc
import decimal
import itertools
@@ -280,10 +280,6 @@ class _CompileLabel(visitors.Visitable):
def type(self):
return self.element.type
- @property
- def quote(self):
- return self.element.quote
-
class SQLCompiler(Compiled):
"""Default implementation of Compiled.
@@ -548,16 +544,14 @@ class SQLCompiler(Compiled):
if is_literal:
name = self.escape_literal_column(name)
else:
- name = self.preparer.quote(name, column.quote)
+ name = self.preparer.quote(name)
table = column.table
if table is None or not include_table or not table.named_with_column:
return name
else:
if table.schema:
- schema_prefix = self.preparer.quote_schema(
- table.schema,
- table.quote_schema) + '.'
+ schema_prefix = self.preparer.quote_schema(table.schema) + '.'
else:
schema_prefix = ''
tablename = table.name
@@ -565,7 +559,7 @@ class SQLCompiler(Compiled):
tablename = self._truncated_identifier("alias", tablename)
return schema_prefix + \
- self.preparer.quote(tablename, table.quote) + \
+ self.preparer.quote(tablename) + \
"." + name
def escape_literal_column(self, text):
@@ -953,7 +947,7 @@ class SQLCompiler(Compiled):
self.binds[bindparam.key] = self.binds[name] = bindparam
- return self.bindparam_string(name, quote=bindparam.quote, **kwargs)
+ return self.bindparam_string(name, **kwargs)
def render_literal_bindparam(self, bindparam, **kw):
value = bindparam.value
@@ -1023,8 +1017,7 @@ class SQLCompiler(Compiled):
self.anon_map[derived] = anonymous_counter + 1
return derived + "_" + str(anonymous_counter)
- def bindparam_string(self, name, quote=None,
- positional_names=None, **kw):
+ def bindparam_string(self, name, positional_names=None, **kw):
if self.positional:
if positional_names is not None:
positional_names.append(name)
@@ -1574,12 +1567,10 @@ class SQLCompiler(Compiled):
fromhints=None, **kwargs):
if asfrom or ashint:
if getattr(table, "schema", None):
- ret = self.preparer.quote_schema(table.schema,
- table.quote_schema) + \
- "." + self.preparer.quote(table.name,
- table.quote)
+ ret = self.preparer.quote_schema(table.schema) + \
+ "." + self.preparer.quote(table.name)
else:
- ret = self.preparer.quote(table.name, table.quote)
+ ret = self.preparer.quote(table.name)
if fromhints and table in fromhints:
ret = self.format_from_hint_text(ret, table,
fromhints[table], iscrud)
@@ -1796,8 +1787,7 @@ class SQLCompiler(Compiled):
if name is None:
name = col.key
bindparam = elements.BindParameter(name, value,
- type_=col.type, required=required,
- quote=col.quote)
+ type_=col.type, required=required)
bindparam._is_crud = True
return bindparam._compiler_dispatch(self)
@@ -2193,11 +2183,11 @@ class DDLCompiler(Compiled):
return self.sql_compiler.post_process_text(ddl.statement % context)
def visit_create_schema(self, create):
- schema = self.preparer.format_schema(create.element, create.quote)
+ schema = self.preparer.format_schema(create.element)
return "CREATE SCHEMA " + schema
def visit_drop_schema(self, drop):
- schema = self.preparer.format_schema(drop.element, drop.quote)
+ schema = self.preparer.format_schema(drop.element)
text = "DROP SCHEMA " + schema
if drop.cascade:
text += " CASCADE"
@@ -2325,8 +2315,7 @@ class DDLCompiler(Compiled):
def _prepared_index_name(self, index, include_schema=False):
if include_schema and index.table is not None and index.table.schema:
schema = index.table.schema
- schema_name = self.preparer.quote_schema(schema,
- index.table.quote_schema)
+ schema_name = self.preparer.quote_schema(schema)
else:
schema_name = None
@@ -2340,9 +2329,7 @@ class DDLCompiler(Compiled):
else:
self.dialect.validate_identifier(ident)
- index_name = self.preparer.quote(
- ident,
- index.quote)
+ index_name = self.preparer.quote(ident)
if schema_name:
index_name = schema_name + "." + index_name
@@ -2424,7 +2411,7 @@ class DDLCompiler(Compiled):
text += "CONSTRAINT %s " % \
self.preparer.format_constraint(constraint)
text += "PRIMARY KEY "
- text += "(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
+ text += "(%s)" % ', '.join(self.preparer.quote(c.name)
for c in constraint)
text += self.define_constraint_deferrability(constraint)
return text
@@ -2437,11 +2424,11 @@ class DDLCompiler(Compiled):
preparer.format_constraint(constraint)
remote_table = list(constraint._elements.values())[0].column.table
text += "FOREIGN KEY(%s) REFERENCES %s (%s)" % (
- ', '.join(preparer.quote(f.parent.name, f.parent.quote)
+ ', '.join(preparer.quote(f.parent.name)
for f in constraint._elements.values()),
self.define_constraint_remote_table(
constraint, remote_table, preparer),
- ', '.join(preparer.quote(f.column.name, f.column.quote)
+ ', '.join(preparer.quote(f.column.name)
for f in constraint._elements.values())
)
text += self.define_constraint_match(constraint)
@@ -2460,7 +2447,7 @@ class DDLCompiler(Compiled):
text += "CONSTRAINT %s " % \
self.preparer.format_constraint(constraint)
text += "UNIQUE (%s)" % (
- ', '.join(self.preparer.quote(c.name, c.quote)
+ ', '.join(self.preparer.quote(c.name)
for c in constraint))
text += self.define_constraint_deferrability(constraint)
return text
@@ -2714,15 +2701,25 @@ class IdentifierPreparer(object):
or not self.legal_characters.match(util.text_type(value))
or (lc_value != value))
- def quote_schema(self, schema, force):
- """Quote a schema.
+ def quote_schema(self, schema, force=None):
+ """Conditionally quote a schema.
+
+ Subclasses can override this to provide database-dependent
+ quoting behavior for schema names.
+
+ the 'force' flag should be considered deprecated.
- Subclasses should override this to provide database-dependent
- quoting behavior.
"""
return self.quote(schema, force)
- def quote(self, ident, force):
+ def quote(self, ident, force=None):
+ """Conditionally quote an identifier.
+
+ the 'force' flag should be considered deprecated.
+ """
+
+ force = getattr(ident, "quote", None)
+
if force is None:
if ident in self._strings:
return self._strings[ident]
@@ -2738,38 +2735,35 @@ class IdentifierPreparer(object):
return ident
def format_sequence(self, sequence, use_schema=True):
- name = self.quote(sequence.name, sequence.quote)
- if not self.omit_schema and use_schema and \
- sequence.schema is not None:
- name = self.quote_schema(sequence.schema, sequence.quote) + \
- "." + name
+ name = self.quote(sequence.name)
+ if not self.omit_schema and use_schema and sequence.schema is not None:
+ name = self.quote_schema(sequence.schema) + "." + name
return name
def format_label(self, label, name=None):
- return self.quote(name or label.name, label.quote)
+ return self.quote(name or label.name)
def format_alias(self, alias, name=None):
- return self.quote(name or alias.name, alias.quote)
+ return self.quote(name or alias.name)
def format_savepoint(self, savepoint, name=None):
- return self.quote(name or savepoint.ident, savepoint.quote)
+ return self.quote(name or savepoint.ident)
def format_constraint(self, constraint):
- return self.quote(constraint.name, constraint.quote)
+ return self.quote(constraint.name)
def format_table(self, table, use_schema=True, name=None):
"""Prepare a quoted table and schema name."""
if name is None:
name = table.name
- result = self.quote(name, table.quote)
+ result = self.quote(name)
if not self.omit_schema and use_schema \
and getattr(table, "schema", None):
- result = self.quote_schema(table.schema, table.quote_schema) + \
- "." + result
+ result = self.quote_schema(table.schema) + "." + result
return result
- def format_schema(self, name, quote):
+ def format_schema(self, name, quote=None):
"""Prepare a quoted schema name."""
return self.quote(name, quote)
@@ -2784,10 +2778,9 @@ class IdentifierPreparer(object):
if use_table:
return self.format_table(
column.table, use_schema=False,
- name=table_name) + "." + \
- self.quote(name, column.quote)
+ name=table_name) + "." + self.quote(name)
else:
- return self.quote(name, column.quote)
+ return self.quote(name)
else:
# literal textual elements get stuck into ColumnClause a lot,
# which shouldn't get quoted
@@ -2807,7 +2800,7 @@ class IdentifierPreparer(object):
if not self.omit_schema and use_schema and \
getattr(table, 'schema', None):
- return (self.quote_schema(table.schema, table.quote_schema),
+ return (self.quote_schema(table.schema),
self.format_table(table, use_schema=False))
else:
return (self.format_table(table, use_schema=False), )
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 17fb40628..99dd193f3 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -530,7 +530,6 @@ class ColumnElement(ClauseElement, operators.ColumnOperators):
__visit_name__ = 'column'
primary_key = False
foreign_keys = []
- quote = None
_label = None
_key_label = None
_alt_names = ()
@@ -693,7 +692,6 @@ class BindParameter(ColumnElement):
"""
__visit_name__ = 'bindparam'
- quote = None
_is_crud = False
@@ -778,6 +776,8 @@ class BindParameter(ColumnElement):
if value is NO_ARG:
value = None
+ if quote is not None:
+ key = quoted_name(key, quote)
if unique:
self.key = _anonymous_label('%%(%d %s)s' % (id(self), key
@@ -800,7 +800,6 @@ class BindParameter(ColumnElement):
self.callable = callable_
self.isoutparam = isoutparam
self.required = required
- self.quote = quote
if type_ is None:
if _compared_to_type is not None:
self.type = \
@@ -1838,7 +1837,6 @@ class Label(ColumnElement):
self.key = self._label = self._key_label = self.name
self._element = element
self._type = type_
- self.quote = element.quote
self._proxies = [element]
@util.memoized_property
@@ -2027,6 +2025,12 @@ class ColumnClause(Immutable, ColumnElement):
else:
label = t.name + "_" + name
+ # propagate name quoting rules for labels.
+ if getattr(name, "quote", None) is not None:
+ label = quoted_name(label, name.quote)
+ elif getattr(t.name, "quote", None) is not None:
+ label = quoted_name(label, t.name.quote)
+
# ensure the label name doesn't conflict with that
# of an existing column
if label in t.c:
@@ -2078,7 +2082,6 @@ class _IdentifiedClause(Executable, ClauseElement):
__visit_name__ = 'identified'
_execution_options = \
Executable._execution_options.union({'autocommit': False})
- quote = None
def __init__(self, ident):
self.ident = ident
@@ -2096,10 +2099,92 @@ class ReleaseSavepointClause(_IdentifiedClause):
__visit_name__ = 'release_savepoint'
-class _truncated_label(util.text_type):
+class quoted_name(util.text_type):
+ """Represent a SQL identifier combined with quoting preferences.
+
+ :class:`.quoted_name` is a Python unicode/str subclass which
+ represents a particular identifier name along with a
+ ``quote`` flag. This ``quote`` flag, when set to
+ ``True`` or ``False``, overrides automatic quoting behavior
+ for this identifier in order to either unconditionally quote
+ or to not quote the name. If left at its default of ``None``,
+ quoting behavior is applied to the identifier on a per-backend basis
+ based on an examination of the token itself.
+
+ A :class:`.quoted_name` object with ``quote=True`` is also
+ prevented from being modified in the case of a so-called
+ "name normalize" option. Certain database backends, such as
+ Oracle, Firebird, and DB2 "normalize" case-insensitive names
+ as uppercase. The SQLAlchemy dialects for these backends
+ convert from SQLAlchemy's lower-case-means-insensitive convention
+ to the upper-case-means-insensitive conventions of those backends.
+ The ``quote=True`` flag here will prevent this conversion from occurring
+ to support an identifier that's quoted as all lower case against
+ such a backend.
+
+ The :class:`.quoted_name` object is normally created automatically
+ when specifying the name for key schema constructs such as :class:`.Table`,
+ :class:`.Column`, and others. The class can also be passed explicitly
+ as the name to any function that receives a name which can be quoted.
+ Such as to use the :meth:`.Engine.has_table` method with an unconditionally
+ quoted name::
+
+ from sqlaclchemy import create_engine
+ from sqlalchemy.sql.elements import quoted_name
+
+ engine = create_engine("oracle+cx_oracle://some_dsn")
+ engine.has_table(quoted_name("some_table", True))
+
+ The above logic will run the "has table" logic against the Oracle backend,
+ passing the name exactly as ``"some_table"`` without converting to
+ upper case.
+
+ .. versionadded:: 0.9.0
+
+ """
+
+ def __new__(cls, value, quote):
+ if value is None:
+ return None
+ elif isinstance(value, cls) and (
+ quote is None or value.quote == quote
+ ):
+ return value
+ self = super(quoted_name, cls).__new__(cls, value)
+ self.quote = quote
+ return self
+
+ def __reduce__(self):
+ return quoted_name, (util.text_type(self), self.quote)
+
+ @util.memoized_instancemethod
+ def lower(self):
+ if self.quote:
+ return self
+ else:
+ return util.text_type(self).lower()
+
+ @util.memoized_instancemethod
+ def upper(self):
+ if self.quote:
+ return self
+ else:
+ return util.text_type(self).upper()
+
+ def __repr__(self):
+ return "'%s'" % self
+
+class _truncated_label(quoted_name):
"""A unicode subclass used to identify symbolic "
"names that may require truncation."""
+ def __new__(cls, value, quote=None):
+ quote = getattr(value, "quote", quote)
+ return super(_truncated_label, cls).__new__(cls, value, quote)
+
+ def __reduce__(self):
+ return self.__class__, (util.text_type(self), self.quote)
+
def apply_map(self, map_):
return self
@@ -2116,16 +2201,25 @@ class _anonymous_label(_truncated_label):
def __add__(self, other):
return _anonymous_label(
- util.text_type(self) +
- util.text_type(other))
+ quoted_name(
+ util.text_type.__add__(self, util.text_type(other)),
+ self.quote)
+ )
def __radd__(self, other):
return _anonymous_label(
- util.text_type(other) +
- util.text_type(self))
+ quoted_name(
+ util.text_type.__add__(util.text_type(other), self),
+ self.quote)
+ )
def apply_map(self, map_):
- return self % map_
+ if self.quote is not None:
+ # preserve quoting only if necessary
+ return quoted_name(self % map_, self.quote)
+ else:
+ # else skip the constructor call
+ return self % map_
def _as_truncated(value):
diff --git a/lib/sqlalchemy/sql/schema.py b/lib/sqlalchemy/sql/schema.py
index ca83236c7..b190c3874 100644
--- a/lib/sqlalchemy/sql/schema.py
+++ b/lib/sqlalchemy/sql/schema.py
@@ -37,7 +37,7 @@ from . import type_api
from .base import _bind_or_error, ColumnCollection
from .elements import ClauseElement, ColumnClause, _truncated_label, \
_as_truncated, TextClause, _literal_as_text,\
- ColumnElement, _find_columns
+ ColumnElement, _find_columns, quoted_name
from .selectable import TableClause
import collections
import sqlalchemy
@@ -67,7 +67,6 @@ class SchemaItem(SchemaEventTarget, visitors.Visitable):
"""Base class for items that define a database schema."""
__visit_name__ = 'schema_item'
- quote = None
def _init_items(self, *args):
"""Initialize the list of child items for this SchemaItem."""
@@ -83,6 +82,17 @@ class SchemaItem(SchemaEventTarget, visitors.Visitable):
def __repr__(self):
return util.generic_repr(self)
+ @property
+ @util.deprecated('0.9', 'Use ``<obj>.name.quote``')
+ def quote(self):
+ """Return the value of the ``quote`` flag passed
+ to this schema object, for those schema items which
+ have a ``name`` field.
+
+ """
+
+ return self.name.quote
+
@util.memoized_property
def info(self):
"""Info dictionary associated with the object, allowing user-defined
@@ -114,25 +124,29 @@ class Table(SchemaItem, TableClause):
a second time will return the *same* :class:`.Table` object - in this way
the :class:`.Table` constructor acts as a registry function.
- See also:
+ .. seealso::
- :ref:`metadata_describing` - Introduction to database metadata
+ :ref:`metadata_describing` - Introduction to database metadata
Constructor arguments are as follows:
:param name: The name of this table as represented in the database.
- This property, along with the *schema*, indicates the *singleton
- identity* of this table in relation to its parent :class:`.MetaData`.
+ The table name, along with the value of the ``schema`` parameter,
+ forms a key which uniquely identifies this :class:`.Table` within
+ the owning :class:`.MetaData` collection.
Additional calls to :class:`.Table` with the same name, metadata,
and schema name will return the same :class:`.Table` object.
Names which contain no upper case characters
will be treated as case insensitive names, and will not be quoted
- unless they are a reserved word. Names with any number of upper
- case characters will be quoted and sent exactly. Note that this
- behavior applies even for databases which standardize upper
- case names as case insensitive such as Oracle.
+ unless they are a reserved word or contain special characters.
+ A name with any number of upper case characters is considered
+ to be case sensitive, and will be sent as quoted.
+
+ To enable unconditional quoting for the table name, specify the flag
+ ``quote=True`` to the constructor, or use the :class:`.quoted_name`
+ construct to specify the name.
:param metadata: a :class:`.MetaData` object which will contain this
table. The metadata is used as a point of association of this table
@@ -263,9 +277,17 @@ class Table(SchemaItem, TableClause):
:param quote_schema: same as 'quote' but applies to the schema identifier.
- :param schema: The *schema name* for this table, which is required if
+ :param schema: The schema name for this table, which is required if
the table resides in a schema other than the default selected schema
- for the engine's database connection. Defaults to ``None``.
+ for the engine's database connection. Defaults to ``None``.
+
+ The quoting rules for the schema name are the same as those for the
+ ``name`` parameter, in that quoting is applied for reserved words or
+ case-sensitive names; to enable unconditional quoting for the
+ schema name, specify the flag
+ ``quote_schema=True`` to the constructor, or use the :class:`.quoted_name`
+ construct to specify the name.
+
:param useexisting: Deprecated. Use extend_existing.
@@ -329,6 +351,15 @@ class Table(SchemaItem, TableClause):
#metadata._remove_table(name, schema)
raise
+
+ @property
+ @util.deprecated('0.9', 'Use ``table.schema.quote``')
+ def quote_schema(self):
+ """Return the value of the ``quote_schema`` flag passed
+ to this :class:`.Table`."""
+
+ return self.schema.quote
+
def __init__(self, *args, **kw):
"""Constructor for :class:`~.schema.Table`.
@@ -341,15 +372,15 @@ class Table(SchemaItem, TableClause):
# calling the superclass constructor.
def _init(self, name, metadata, *args, **kwargs):
- super(Table, self).__init__(name)
+ super(Table, self).__init__(quoted_name(name, kwargs.pop('quote', None)))
self.metadata = metadata
+
self.schema = kwargs.pop('schema', None)
if self.schema is None:
self.schema = metadata.schema
- self.quote_schema = kwargs.pop(
- 'quote_schema', metadata.quote_schema)
else:
- self.quote_schema = kwargs.pop('quote_schema', None)
+ quote_schema = kwargs.pop('quote_schema', None)
+ self.schema = quoted_name(self.schema, quote_schema)
self.indexes = set()
self.constraints = set()
@@ -370,7 +401,7 @@ class Table(SchemaItem, TableClause):
include_columns = kwargs.pop('include_columns', None)
self.implicit_returning = kwargs.pop('implicit_returning', True)
- self.quote = kwargs.pop('quote', None)
+
if 'info' in kwargs:
self.info = kwargs.pop('info')
if 'listeners' in kwargs:
@@ -444,7 +475,8 @@ class Table(SchemaItem, TableClause):
for key in ('quote', 'quote_schema'):
if key in kwargs:
- setattr(self, key, kwargs.pop(key))
+ raise exc.ArgumentError(
+ "Can't redefine 'quote' or 'quote_schema' arguments")
if 'info' in kwargs:
self.info = kwargs.pop('info')
@@ -597,7 +629,9 @@ class Table(SchemaItem, TableClause):
:class:`.Table`, using the given :class:`.Connectable`
for connectivity.
- See also :meth:`.MetaData.create_all`.
+ .. seealso::
+
+ :meth:`.MetaData.create_all`.
"""
@@ -612,7 +646,9 @@ class Table(SchemaItem, TableClause):
:class:`.Table`, using the given :class:`.Connectable`
for connectivity.
- See also :meth:`.MetaData.drop_all`.
+ .. seealso::
+
+ :meth:`.MetaData.drop_all`.
"""
if bind is None:
@@ -925,6 +961,12 @@ class Column(SchemaItem, ColumnClause):
"May not pass type_ positionally and as a keyword.")
type_ = args.pop(0)
+ if name is not None:
+ name = quoted_name(name, kwargs.pop('quote', None))
+ elif "quote" in kwargs:
+ raise exc.ArgumentError("Explicit 'name' is required when "
+ "sending 'quote' argument")
+
super(Column, self).__init__(name, type_)
self.key = kwargs.pop('key', name)
self.primary_key = kwargs.pop('primary_key', False)
@@ -935,7 +977,6 @@ class Column(SchemaItem, ColumnClause):
self.index = kwargs.pop('index', None)
self.unique = kwargs.pop('unique', None)
self.system = kwargs.pop('system', False)
- self.quote = kwargs.pop('quote', None)
self.doc = kwargs.pop('doc', None)
self.onupdate = kwargs.pop('onupdate', None)
self.autoincrement = kwargs.pop('autoincrement', True)
@@ -988,6 +1029,10 @@ class Column(SchemaItem, ColumnClause):
raise exc.ArgumentError(
"Unknown arguments passed to Column: " + repr(list(kwargs)))
+# @property
+# def quote(self):
+# return getattr(self.name, "quote", None)
+
def __str__(self):
if self.name is None:
return "(no name)"
@@ -1123,7 +1168,7 @@ class Column(SchemaItem, ColumnClause):
nullable=self.nullable,
unique=self.unique,
system=self.system,
- quote=self.quote,
+ #quote=self.quote,
index=self.index,
autoincrement=self.autoincrement,
default=self.default,
@@ -1161,7 +1206,6 @@ class Column(SchemaItem, ColumnClause):
key=key if key else name if name else self.key,
primary_key=self.primary_key,
nullable=self.nullable,
- quote=self.quote,
_proxies=[self], *fk)
except TypeError:
util.raise_from_cause(
@@ -1791,7 +1835,11 @@ class Sequence(DefaultGenerator):
be emitted as well. For platforms that don't support sequences,
the :class:`.Sequence` construct is ignored.
- See also: :class:`.CreateSequence` :class:`.DropSequence`
+ .. seealso::
+
+ :class:`.CreateSequence`
+
+ :class:`.DropSequence`
"""
@@ -1828,6 +1876,8 @@ class Sequence(DefaultGenerator):
forces quoting of the schema name on or off. When left at its
default of ``None``, normal quoting rules based on casing and reserved
words take place.
+ :param quote_schema: set the quoting preferences for the ``schema``
+ name.
:param metadata: optional :class:`.MetaData` object which will be
associated with this :class:`.Sequence`. A :class:`.Sequence`
that is associated with a :class:`.MetaData` gains access to the
@@ -1855,17 +1905,14 @@ class Sequence(DefaultGenerator):
"""
super(Sequence, self).__init__(for_update=for_update)
- self.name = name
+ self.name = quoted_name(name, quote)
self.start = start
self.increment = increment
self.optional = optional
- self.quote = quote
if metadata is not None and schema is None and metadata.schema:
self.schema = schema = metadata.schema
- self.quote_schema = metadata.quote_schema
else:
- self.schema = schema
- self.quote_schema = quote_schema
+ self.schema = quoted_name(schema, quote_schema)
self.metadata = metadata
self._key = _get_table_key(name, schema)
if metadata:
@@ -2556,7 +2603,7 @@ class Index(ColumnCollectionMixin, SchemaItem):
# objects are present
ColumnCollectionMixin.__init__(self, *columns)
- self.name = name
+ self.name = quoted_name(name, kw.pop("quote", None))
self.unique = kw.pop('unique', False)
self.kwargs = kw
@@ -2598,7 +2645,9 @@ class Index(ColumnCollectionMixin, SchemaItem):
:class:`.Index`, using the given :class:`.Connectable`
for connectivity.
- See also :meth:`.MetaData.create_all`.
+ .. seealso::
+
+ :meth:`.MetaData.create_all`.
"""
if bind is None:
@@ -2611,7 +2660,9 @@ class Index(ColumnCollectionMixin, SchemaItem):
:class:`.Index`, using the given :class:`.Connectable`
for connectivity.
- See also :meth:`.MetaData.drop_all`.
+ .. seealso::
+
+ :meth:`.MetaData.drop_all`.
"""
if bind is None:
@@ -2653,12 +2704,9 @@ class MetaData(SchemaItem):
MetaData is a thread-safe object after tables have been explicitly defined
or loaded via reflection.
- See also:
-
- :ref:`metadata_describing` - Introduction to database metadata
+ .. seealso::
- .. index::
- single: thread safety; MetaData
+ :ref:`metadata_describing` - Introduction to database metadata
"""
@@ -2695,8 +2743,7 @@ class MetaData(SchemaItem):
"""
self.tables = util.immutabledict()
- self.schema = schema
- self.quote_schema = quote_schema
+ self.schema = quoted_name(schema, quote_schema)
self._schemas = set()
self._sequences = {}
self._fk_memos = collections.defaultdict(list)
@@ -2742,7 +2789,6 @@ class MetaData(SchemaItem):
def __getstate__(self):
return {'tables': self.tables,
'schema': self.schema,
- 'quote_schema': self.quote_schema,
'schemas': self._schemas,
'sequences': self._sequences,
'fk_memos': self._fk_memos}
@@ -2750,7 +2796,6 @@ class MetaData(SchemaItem):
def __setstate__(self, state):
self.tables = state['tables']
self.schema = state['schema']
- self.quote_schema = state['quote_schema']
self._bind = None
self._sequences = state['sequences']
self._schemas = state['schemas']
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index c32de77ea..e06262c6d 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -136,7 +136,6 @@ class FromClause(Selectable):
__visit_name__ = 'fromclause'
named_with_column = False
_hide_froms = []
- quote = None
schema = None
_memoized_property = util.group_expirable_memoized_property(["_columns"])
diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py
index 38e0d1bd3..db0ad248c 100644
--- a/lib/sqlalchemy/sql/sqltypes.py
+++ b/lib/sqlalchemy/sql/sqltypes.py
@@ -12,6 +12,7 @@ import datetime as dt
import codecs
from .type_api import TypeEngine, TypeDecorator, to_instance
+from .elements import quoted_name
from .default_comparator import _DefaultColumnComparator
from .. import exc, util, processors
from .base import _bind_or_error, SchemaEventTarget
@@ -840,8 +841,11 @@ class SchemaType(SchemaEventTarget):
"""
def __init__(self, **kw):
- self.name = kw.pop('name', None)
- self.quote = kw.pop('quote', None)
+ name = kw.pop('name', None)
+ if name is not None:
+ self.name = quoted_name(name, kw.pop('quote', None))
+ else:
+ self.name = None
self.schema = kw.pop('schema', None)
self.metadata = kw.pop('metadata', None)
self.inherit_schema = kw.pop('inherit_schema', False)
@@ -896,7 +900,6 @@ class SchemaType(SchemaEventTarget):
schema = kw.pop('schema', self.schema)
metadata = kw.pop('metadata', self.metadata)
return impltype(name=self.name,
- quote=self.quote,
schema=schema,
metadata=metadata,
inherit_schema=self.inherit_schema,
@@ -1008,10 +1011,7 @@ class Enum(String, SchemaType):
owning :class:`.Table`. If this behavior is desired,
set the ``inherit_schema`` flag to ``True``.
- :param quote: Force quoting to be on or off on the type's name. If
- left as the default of `None`, the usual schema-level "case
- sensitive"/"reserved name" rules are used to determine if this
- type's name should be quoted.
+ :param quote: Set explicit quoting preferences for the type's name.
:param inherit_schema: When ``True``, the "schema" from the owning
:class:`.Table` will be copied to the "schema" attribute of this
@@ -1071,7 +1071,6 @@ class Enum(String, SchemaType):
metadata = kw.pop('metadata', self.metadata)
if issubclass(impltype, Enum):
return impltype(name=self.name,
- quote=self.quote,
schema=schema,
metadata=metadata,
convert_unicode=self.convert_unicode,
diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py
index b927f1b3c..2a5c2e277 100644
--- a/lib/sqlalchemy/sql/util.py
+++ b/lib/sqlalchemy/sql/util.py
@@ -269,7 +269,6 @@ def expression_as_ddl(clause):
elif isinstance(element, ColumnClause) and \
element.table is not None:
col = ColumnClause(element.name)
- col.quote = element.quote
return col
else:
return None