summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-02-05 14:22:55 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2012-02-05 14:22:55 -0500
commita4e3bc61bcb1f1aeaa334f6da4f3b9fcb3059d00 (patch)
treea1c1e25d24e6a65c7a368a85125818975f28c59a /lib/sqlalchemy
parente0ec05366f7363edd1873c4d095e11151cdd4dff (diff)
downloadsqlalchemy-a4e3bc61bcb1f1aeaa334f6da4f3b9fcb3059d00.tar.gz
- [bug] A significant change to how labeling
is applied to columns in SELECT statements allows "truncated" labels, that is label names that are generated in Python which exceed the maximum identifier length (note this is configurable via label_length on create_engine()), to be properly referenced when rendered inside of a subquery, as well as to be present in a result set row using their original in-Python names. [ticket:2396] - apply pep8 to test_labels
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/dialects/firebird/base.py2
-rw-r--r--lib/sqlalchemy/dialects/oracle/base.py2
-rw-r--r--lib/sqlalchemy/dialects/sqlite/base.py25
-rw-r--r--lib/sqlalchemy/engine/base.py37
-rw-r--r--lib/sqlalchemy/engine/default.py5
-rw-r--r--lib/sqlalchemy/schema.py4
-rw-r--r--lib/sqlalchemy/sql/compiler.py20
-rw-r--r--lib/sqlalchemy/sql/expression.py74
8 files changed, 111 insertions, 58 deletions
diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py
index 8cf2ded2f..031c68919 100644
--- a/lib/sqlalchemy/dialects/firebird/base.py
+++ b/lib/sqlalchemy/dialects/firebird/base.py
@@ -215,7 +215,7 @@ class FBCompiler(sql.compiler.SQLCompiler):
# Override to not use the AS keyword which FB 1.5 does not like
if asfrom:
alias_name = isinstance(alias.name,
- expression._generated_label) and \
+ expression._truncated_label) and \
self._truncated_identifier("alias",
alias.name) or alias.name
diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py
index 88e506287..56838f803 100644
--- a/lib/sqlalchemy/dialects/oracle/base.py
+++ b/lib/sqlalchemy/dialects/oracle/base.py
@@ -481,7 +481,7 @@ class OracleCompiler(compiler.SQLCompiler):
"""Oracle doesn't like ``FROM table AS alias``. Is the AS standard SQL??"""
if asfrom or ashint:
- alias_name = isinstance(alias.name, expression._generated_label) and \
+ alias_name = isinstance(alias.name, expression._truncated_label) and \
self._truncated_identifier("alias", alias.name) or alias.name
if ashint:
diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py
index f9520affa..10a0d882b 100644
--- a/lib/sqlalchemy/dialects/sqlite/base.py
+++ b/lib/sqlalchemy/dialects/sqlite/base.py
@@ -441,20 +441,6 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer):
result = self.quote_schema(index.table.schema, index.table.quote_schema) + "." + result
return result
-class SQLiteExecutionContext(default.DefaultExecutionContext):
- def get_result_proxy(self):
- rp = base.ResultProxy(self)
- if rp._metadata:
- # adjust for dotted column names. SQLite
- # in the case of UNION may store col names as
- # "tablename.colname"
- # in cursor.description
- for colname in rp._metadata.keys:
- if "." in colname:
- trunc_col = colname.split(".")[1]
- rp._metadata._set_keymap_synonym(trunc_col, colname)
- return rp
-
class SQLiteDialect(default.DefaultDialect):
name = 'sqlite'
supports_alter = False
@@ -472,7 +458,6 @@ class SQLiteDialect(default.DefaultDialect):
ischema_names = ischema_names
colspecs = colspecs
isolation_level = None
- execution_ctx_cls = SQLiteExecutionContext
supports_cast = True
supports_default_values = True
@@ -540,6 +525,16 @@ class SQLiteDialect(default.DefaultDialect):
else:
return None
+ def _translate_colname(self, colname):
+ # adjust for dotted column names. SQLite
+ # in the case of UNION may store col names as
+ # "tablename.colname"
+ # in cursor.description
+ if "." in colname:
+ return colname.split(".")[1], colname
+ else:
+ return colname, None
+
@reflection.cache
def get_table_names(self, connection, schema=None, **kw):
if schema is not None:
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index db19fe7de..3175ebabd 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -2575,6 +2575,10 @@ class ResultMetaData(object):
context = parent.context
dialect = context.dialect
typemap = dialect.dbapi_type_map
+ translate_colname = dialect._translate_colname
+
+ # high precedence key values.
+ primary_keymap = {}
for i, rec in enumerate(metadata):
colname = rec[0]
@@ -2583,6 +2587,9 @@ class ResultMetaData(object):
if dialect.description_encoding:
colname = dialect._description_decoder(colname)
+ if translate_colname:
+ colname, untranslated = translate_colname(colname)
+
if context.result_map:
try:
name, obj, type_ = context.result_map[colname.lower()]
@@ -2600,15 +2607,17 @@ class ResultMetaData(object):
# indexes as keys. This is only needed for the Python version of
# RowProxy (the C version uses a faster path for integer indexes).
- keymap[i] = rec
-
- # Column names as keys
- if keymap.setdefault(name.lower(), rec) is not rec:
- # We do not raise an exception directly because several
- # columns colliding by name is not a problem as long as the
- # user does not try to access them (ie use an index directly,
- # or the more precise ColumnElement)
- keymap[name.lower()] = (processor, obj, None)
+ primary_keymap[i] = rec
+
+ # populate primary keymap, looking for conflicts.
+ if primary_keymap.setdefault(name.lower(), rec) is not rec:
+ # place a record that doesn't have the "index" - this
+ # is interpreted later as an AmbiguousColumnError,
+ # but only when actually accessed. Columns
+ # colliding by name is not a problem if those names
+ # aren't used; integer and ColumnElement access is always
+ # unambiguous.
+ primary_keymap[name.lower()] = (processor, obj, None)
if dialect.requires_name_normalize:
colname = dialect.normalize_name(colname)
@@ -2618,10 +2627,20 @@ class ResultMetaData(object):
for o in obj:
keymap[o] = rec
+ if translate_colname and \
+ untranslated:
+ keymap[untranslated] = rec
+
+ # overwrite keymap values with those of the
+ # high precedence keymap.
+ keymap.update(primary_keymap)
+
if parent._echo:
context.engine.logger.debug(
"Col %r", tuple(x[0] for x in metadata))
+ @util.pending_deprecation("0.8", "sqlite dialect uses "
+ "_translate_colname() now")
def _set_keymap_synonym(self, name, origname):
"""Set a synonym for the given name.
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index 73bd7fd71..a1e5a5799 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -44,6 +44,7 @@ class DefaultDialect(base.Dialect):
postfetch_lastrowid = True
implicit_returning = False
+
supports_native_enum = False
supports_native_boolean = False
@@ -95,6 +96,10 @@ class DefaultDialect(base.Dialect):
# and denormalize_name() must be provided.
requires_name_normalize = False
+ # a hook for SQLite's translation of
+ # result column names
+ _translate_colname = None
+
reflection_options = ()
def __init__(self, convert_unicode=False, assert_unicode=False,
diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py
index f0a929776..c183e4385 100644
--- a/lib/sqlalchemy/schema.py
+++ b/lib/sqlalchemy/schema.py
@@ -1028,7 +1028,7 @@ class Column(SchemaItem, expression.ColumnClause):
"The 'index' keyword argument on Column is boolean only. "
"To create indexes with a specific name, create an "
"explicit Index object external to the Table.")
- Index(expression._generated_label('ix_%s' % self._label), self, unique=self.unique)
+ Index(expression._truncated_label('ix_%s' % self._label), self, unique=self.unique)
elif self.unique:
if isinstance(self.unique, basestring):
raise exc.ArgumentError(
@@ -1093,7 +1093,7 @@ class Column(SchemaItem, expression.ColumnClause):
"been assigned.")
try:
c = self._constructor(
- name or self.name,
+ expression._as_truncated(name or self.name),
self.type,
key = name or self.key,
primary_key = self.primary_key,
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index b0a55b886..598e87932 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -354,7 +354,7 @@ class SQLCompiler(engine.Compiled):
# or ORDER BY clause of a select. dialect-specific compilers
# can modify this behavior.
if within_columns_clause and not within_label_clause:
- if isinstance(label.name, sql._generated_label):
+ if isinstance(label.name, sql._truncated_label):
labelname = self._truncated_identifier("colident", label.name)
else:
labelname = label.name
@@ -376,17 +376,17 @@ class SQLCompiler(engine.Compiled):
**kw)
def visit_column(self, column, result_map=None, **kwargs):
- name = column.name
+ name = orig_name = column.name
if name is None:
raise exc.CompileError("Cannot compile Column object until "
"it's 'name' is assigned.")
is_literal = column.is_literal
- if not is_literal and isinstance(name, sql._generated_label):
+ if not is_literal and isinstance(name, sql._truncated_label):
name = self._truncated_identifier("colident", name)
if result_map is not None:
- result_map[name.lower()] = (name, (column, ), column.type)
+ result_map[name.lower()] = (orig_name, (column, name), column.type)
if is_literal:
name = self.escape_literal_column(name)
@@ -404,7 +404,7 @@ class SQLCompiler(engine.Compiled):
else:
schema_prefix = ''
tablename = table.name
- if isinstance(tablename, sql._generated_label):
+ if isinstance(tablename, sql._truncated_label):
tablename = self._truncated_identifier("alias", tablename)
return schema_prefix + \
@@ -703,7 +703,7 @@ class SQLCompiler(engine.Compiled):
return self.bind_names[bindparam]
bind_name = bindparam.key
- if isinstance(bind_name, sql._generated_label):
+ if isinstance(bind_name, sql._truncated_label):
bind_name = self._truncated_identifier("bindparam", bind_name)
# add to bind_names for translation
@@ -715,7 +715,7 @@ class SQLCompiler(engine.Compiled):
if (ident_class, name) in self.truncated_names:
return self.truncated_names[(ident_class, name)]
- anonname = name % self.anon_map
+ anonname = name.apply_map(self.anon_map)
if len(anonname) > self.label_length:
counter = self.truncated_names.get(ident_class, 1)
@@ -747,7 +747,7 @@ class SQLCompiler(engine.Compiled):
def visit_alias(self, alias, asfrom=False, ashint=False,
fromhints=None, **kwargs):
if asfrom or ashint:
- if isinstance(alias.name, sql._generated_label):
+ if isinstance(alias.name, sql._truncated_label):
alias_name = self._truncated_identifier("alias", alias.name)
else:
alias_name = alias.name
@@ -784,7 +784,7 @@ class SQLCompiler(engine.Compiled):
not column.is_literal and \
column.table is not None and \
not isinstance(column.table, sql.Select):
- return _CompileLabel(column, sql._generated_label(column.name))
+ return _CompileLabel(column, sql._as_truncated(column.name))
elif not isinstance(column,
(sql._UnaryExpression, sql._TextClause)) \
and (not hasattr(column, 'name') or \
@@ -1445,7 +1445,7 @@ class DDLCompiler(engine.Compiled):
return "\nDROP TABLE " + self.preparer.format_table(drop.element)
def _index_identifier(self, ident):
- if isinstance(ident, sql._generated_label):
+ if isinstance(ident, sql._truncated_label):
max = self.dialect.max_index_name_length or \
self.dialect.max_identifier_length
if len(ident) > max:
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 859ee0437..939456b9a 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -1283,14 +1283,48 @@ func = _FunctionGenerator()
# TODO: use UnaryExpression for this instead ?
modifier = _FunctionGenerator(group=False)
-class _generated_label(unicode):
- """A unicode subclass used to identify dynamically generated names."""
+class _truncated_label(unicode):
+ """A unicode subclass used to identify symbolic "
+ "names that may require truncation."""
-def _escape_for_generated(x):
- if isinstance(x, _generated_label):
- return x
+ def apply_map(self, map_):
+ return self
+
+# for backwards compatibility in case
+# someone is re-implementing the
+# _truncated_identifier() sequence in a custom
+# compiler
+_generated_label = _truncated_label
+
+class _anonymous_label(_truncated_label):
+ """A unicode subclass used to identify anonymously
+ generated names."""
+
+ def __add__(self, other):
+ return _anonymous_label(
+ unicode(self) +
+ unicode(other))
+
+ def __radd__(self, other):
+ return _anonymous_label(
+ unicode(other) +
+ unicode(self))
+
+ def apply_map(self, map_):
+ return self % map_
+
+def _as_truncated(value):
+ """coerce the given value to :class:`._truncated_label`.
+
+ Existing :class:`._truncated_label` and
+ :class:`._anonymous_label` objects are passed
+ unchanged.
+ """
+
+ if isinstance(value, _truncated_label):
+ return value
else:
- return x.replace('%', '%%')
+ return _truncated_label(value)
def _string_or_unprintable(element):
if isinstance(element, basestring):
@@ -2117,7 +2151,9 @@ class ColumnElement(ClauseElement, _CompareMixin):
else:
key = name
- co = ColumnClause(name, selectable, type_=getattr(self,
+ co = ColumnClause(_as_truncated(name),
+ selectable,
+ type_=getattr(self,
'type', None))
co.proxies = [self]
selectable._columns[key] = co
@@ -2165,7 +2201,7 @@ class ColumnElement(ClauseElement, _CompareMixin):
expressions and function calls.
"""
- return _generated_label('%%(%d %s)s' % (id(self), getattr(self,
+ return _anonymous_label('%%(%d %s)s' % (id(self), getattr(self,
'name', 'anon')))
class ColumnCollection(util.OrderedProperties):
@@ -2588,10 +2624,10 @@ class _BindParamClause(ColumnElement):
"""
if unique:
- self.key = _generated_label('%%(%d %s)s' % (id(self), key
+ self.key = _anonymous_label('%%(%d %s)s' % (id(self), key
or 'param'))
else:
- self.key = key or _generated_label('%%(%d param)s'
+ self.key = key or _anonymous_label('%%(%d param)s'
% id(self))
# identifiying key that won't change across
@@ -2639,14 +2675,14 @@ class _BindParamClause(ColumnElement):
def _clone(self):
c = ClauseElement._clone(self)
if self.unique:
- c.key = _generated_label('%%(%d %s)s' % (id(c), c._orig_key
+ c.key = _anonymous_label('%%(%d %s)s' % (id(c), c._orig_key
or 'param'))
return c
def _convert_to_unique(self):
if not self.unique:
self.unique = True
- self.key = _generated_label('%%(%d %s)s' % (id(self),
+ self.key = _anonymous_label('%%(%d %s)s' % (id(self),
self._orig_key or 'param'))
def compare(self, other, **kw):
@@ -3615,7 +3651,7 @@ class Alias(FromClause):
if name is None:
if self.original.named_with_column:
name = getattr(self.original, 'name', None)
- name = _generated_label('%%(%d %s)s' % (id(self), name
+ name = _anonymous_label('%%(%d %s)s' % (id(self), name
or 'anon'))
self.name = name
@@ -3816,7 +3852,7 @@ class _Label(ColumnElement):
while isinstance(element, _Label):
element = element.element
self.name = self.key = self._label = name \
- or _generated_label('%%(%d %s)s' % (id(self),
+ or _anonymous_label('%%(%d %s)s' % (id(self),
getattr(element, 'name', 'anon')))
self._element = element
self._type = type_
@@ -3973,11 +4009,9 @@ class ColumnClause(_Immutable, ColumnElement):
elif t is not None and t.named_with_column:
if getattr(t, 'schema', None):
label = t.schema.replace('.', '_') + "_" + \
- _escape_for_generated(t.name) + "_" + \
- _escape_for_generated(self.name)
+ t.name + "_" + self.name
else:
- label = _escape_for_generated(t.name) + "_" + \
- _escape_for_generated(self.name)
+ label = t.name + "_" + self.name
# ensure the label name doesn't conflict with that
# of an existing column
@@ -3989,7 +4023,7 @@ class ColumnClause(_Immutable, ColumnElement):
counter += 1
label = _label
- return _generated_label(label)
+ return _as_truncated(label)
else:
return self.name
@@ -4018,7 +4052,7 @@ class ColumnClause(_Immutable, ColumnElement):
# otherwise its considered to be a label
is_literal = self.is_literal and (name is None or name == self.name)
c = self._constructor(
- name or self.name,
+ _as_truncated(name or self.name),
selectable=selectable,
type_=self.type,
is_literal=is_literal