to be based solely on presence of cursor.description.
All the regexp-based guessing about statements returning rows
has been removed [ticket:1212].
-
+
+ - Dialects can now generate label names of adjustable length.
+ Pass in the argument "label_length=<value>" to create_engine()
+ to adjust how many characters max will be present in dynamically
+ generated column labels, i.e. "somecolumn AS somelabel". Any
+ value less than 6 will result in a label of minimal size,
+ consiting of an underscore and a numeric counter.
+ The compiler uses the value of dialect.max_identifier_length
+ as a default. [ticket:1211]
+
- Further simplified SELECT compilation and its relationship to
result row processing.
* **echo=False** - if True, the Engine will log all statements as well as a repr() of their parameter lists to the engines logger, which defaults to sys.stdout. The `echo` attribute of `Engine` can be modified at any time to turn logging on and off. If set to the string `"debug"`, result rows will be printed to the standard output as well. This flag ultimately controls a Python logger; see [dbengine_logging](rel:dbengine_logging) at the end of this chapter for information on how to configure logging directly.
* **echo_pool=False** - if True, the connection pool will log all checkouts/checkins to the logging stream, which defaults to sys.stdout. This flag ultimately controls a Python logger; see [dbengine_logging](rel:dbengine_logging) for information on how to configure logging directly.
* **encoding='utf-8'** - the encoding to use for all Unicode translations, both by engine-wide unicode conversion as well as the `Unicode` type object.
+* **label_length=None** - optional integer value which limits the size of dynamically generated column labels to that many characters. If less than 6, labels are generated as "_<counter>". If `None`, the value of `dialect.max_identifier_length` is used instead.
* **module=None** - used by database implementations which support multiple DBAPI modules, this is a reference to a DBAPI2 module to be used instead of the engine's default module. For Postgres, the default is psycopg2. For Oracle, it's cx_Oracle.
* **pool=None** - an already-constructed instance of `sqlalchemy.pool.Pool`, such as a `QueuePool` instance. If non-None, this pool will be used directly as the underlying connection pool for the engine, bypassing whatever connection parameters are present in the URL argument. For information on constructing connection pools manually, see [pooling](rel:pooling).
* **poolclass=None** - a `sqlalchemy.pool.Pool` subclass, which will be used to create a connection pool instance using the connection parameters given in the URL. Note this differs from `pool` in that you don't actually instantiate the pool in this case, you just indicate what type of pool to be used.
supports_default_values = False
supports_empty_insert = True
- def __init__(self, convert_unicode=False, assert_unicode=False, encoding='utf-8', paramstyle=None, dbapi=None, **kwargs):
+ def __init__(self, convert_unicode=False, assert_unicode=False, encoding='utf-8', paramstyle=None, dbapi=None, label_length=None, **kwargs):
self.convert_unicode = convert_unicode
self.assert_unicode = assert_unicode
self.encoding = encoding
self.paramstyle = self.default_paramstyle
self.positional = self.paramstyle in ('qmark', 'format', 'numeric')
self.identifier_preparer = self.preparer(self)
+ if label_length and label_length > self.max_identifier_length:
+ raise exc.ArgumentError("Label length of %d is greater than this dialect's maximum identifier length of %d" % (label_length, self.max_identifier_length))
+ self.label_length = label_length
def create_execution_context(self, connection, **kwargs):
return DefaultExecutionContext(self, connection, **kwargs)
try:
params[_get_params[primary_key].key] = ident[i]
except IndexError:
- raise sa_exc.InvalidRequestError("Could not find enough values to formulate primary key for query.get(); primary key columns are %s" % ', '.join("'%s'" % str(c) for c in q.mapper.primary_key))
+ raise sa_exc.InvalidRequestError("Could not find enough values to formulate primary key for "
+ "query.get(); primary key columns are %s" % ', '.join("'%s'" % c for c in q.mapper.primary_key))
q._params = params
if lockmode is not None:
def _execute_and_instances(self, context):
if self._shard_id is not None:
- result = self.session.connection(mapper=self._mapper_zero(), shard_id=self._shard_id).execute(context.statement, **self._params)
+ result = self.session.connection(mapper=self._mapper_zero(), shard_id=self._shard_id).execute(context.statement, self._params)
try:
return iter(self.instances(result, context))
finally:
else:
partial = []
for shard_id in self.query_chooser(self):
- result = self.session.connection(mapper=self._mapper_zero(), shard_id=shard_id).execute(context.statement, **self._params)
+ result = self.session.connection(mapper=self._mapper_zero(), shard_id=shard_id).execute(context.statement, self._params)
try:
partial = partial + list(self.instances(result, context))
finally:
BIND_PARAMS = re.compile(r'(?<![:\w\$\x5c]):([\w\$]+)(?![:\w\$])', re.UNICODE)
BIND_PARAMS_ESC = re.compile(r'\x5c(:[\w\$]+)(?![:\w\$])', re.UNICODE)
-ANONYMOUS_LABEL = re.compile(r'{ANON (-?\d+) ([^{}]+)}')
BIND_TEMPLATES = {
'pyformat':"%%(%(name)s)s",
Compiles ClauseElements into SQL strings. Uses a similar visit
paradigm as visitors.ClauseVisitor but implements its own traversal.
+
"""
operators = OPERATORS
column_keys
a list of column names to be compiled into an INSERT or UPDATE
statement.
- """
+ """
super(DefaultCompiler, self).__init__(dialect, statement, column_keys, **kwargs)
# if we are insert/update/delete. set to true when we visit an INSERT, UPDATE or DELETE
# ResultProxy uses this for type processing and column targeting
self.result_map = {}
- # a dictionary of ClauseElement subclasses to counters, which are used to
- # generate truncated identifier names or "anonymous" identifiers such as
- # for aliases
- self.generated_ids = {}
-
# true if the paramstyle is positional
self.positional = self.dialect.positional
if self.positional:
# an IdentifierPreparer that formats the quoting of identifiers
self.preparer = self.dialect.identifier_preparer
+ self.label_length = self.dialect.label_length or self.dialect.max_identifier_length
+
+ # a map which tracks "anonymous" identifiers that are
+ # created on the fly here
+ self.anon_map = util.PopulateDict(self._process_anon)
+
+ # a map which tracks "truncated" names based on dialect.label_length
+ # or dialect.max_identifier_length
+ self.truncated_names = {}
+
def compile(self):
self.string = self.process(self.statement)
"""Called when a SELECT statement has no froms, and no FROM clause is to be appended.
Gives Oracle a chance to tack on a ``FROM DUAL`` to the string output.
- """
+ """
return ""
def visit_grouping(self, grouping, **kwargs):
# or ORDER BY clause of a select. dialect-specific compilers
# can modify this behavior.
if within_columns_clause:
- labelname = self._truncated_identifier("colident", label.name)
+ labelname = isinstance(label.name, sql._generated_label) and \
+ self._truncated_identifier("colident", label.name) or label.name
if result_map is not None:
result_map[labelname.lower()] = (label.name, (label, label.element, labelname), label.element.type)
- return " ".join([self.process(label.element), self.operator_string(operators.as_), self.preparer.format_label(label, labelname)])
+ return self.process(label.element) + " " + \
+ self.operator_string(operators.as_) + " " + \
+ self.preparer.format_label(label, labelname)
else:
return self.process(label.element)
def visit_column(self, column, result_map=None, **kwargs):
-
- if not column.is_literal:
- name = self._truncated_identifier("colident", column.name)
- else:
- name = column.name
+ name = column.name
+ if not column.is_literal and isinstance(name, sql._generated_label):
+ name = self._truncated_identifier("colident", name)
if result_map is not None:
result_map[name.lower()] = (name, (column, ), column.type)
schema_prefix = self.preparer.quote(column.table.schema, column.table.quote_schema) + '.'
else:
schema_prefix = ''
- return schema_prefix + self.preparer.quote(ANONYMOUS_LABEL.sub(self._process_anon, column.table.name), column.table.quote) + "." + name
+ return schema_prefix + self.preparer.quote(column.table.name % self.anon_map, column.table.quote) + "." + name
def escape_literal_column(self, text):
"""provide escaping for the literal_column() construct."""
return self.bind_names[bindparam]
bind_name = bindparam.key
- bind_name = self._truncated_identifier("bindparam", bind_name)
+ bind_name = isinstance(bind_name, sql._generated_label) and \
+ self._truncated_identifier("bindparam", bind_name) or bind_name
# add to bind_names for translation
self.bind_names[bindparam] = bind_name
return bind_name
def _truncated_identifier(self, ident_class, name):
- if (ident_class, name) in self.generated_ids:
- return self.generated_ids[(ident_class, name)]
+ if (ident_class, name) in self.truncated_names:
+ return self.truncated_names[(ident_class, name)]
- anonname = ANONYMOUS_LABEL.sub(self._process_anon, name)
+ anonname = name % self.anon_map
- if len(anonname) > self.dialect.max_identifier_length:
- counter = self.generated_ids.get(ident_class, 1)
- truncname = anonname[0:self.dialect.max_identifier_length - 6] + "_" + hex(counter)[2:]
- self.generated_ids[ident_class] = counter + 1
+ if len(anonname) > self.label_length:
+ counter = self.truncated_names.get(ident_class, 1)
+ truncname = anonname[0:max(self.label_length - 6, 0)] + "_" + hex(counter)[2:]
+ self.truncated_names[ident_class] = counter + 1
else:
truncname = anonname
- self.generated_ids[(ident_class, name)] = truncname
+ self.truncated_names[(ident_class, name)] = truncname
return truncname
- def _process_anon(self, match):
- (ident, derived) = match.group(1, 2)
-
- key = ('anonymous', ident)
- if key in self.generated_ids:
- return self.generated_ids[key]
- else:
- anonymous_counter = self.generated_ids.get(('anon_counter', derived), 1)
- newname = derived + "_" + str(anonymous_counter)
- self.generated_ids[('anon_counter', derived)] = anonymous_counter + 1
- self.generated_ids[key] = newname
- return newname
-
def _anonymize(self, name):
- return ANONYMOUS_LABEL.sub(self._process_anon, name)
+ return name % self.anon_map
+
+ def _process_anon(self, key):
+ (ident, derived) = key.split(' ')
+
+ anonymous_counter = self.anon_map.get(derived, 1)
+ self.anon_map[derived] = anonymous_counter + 1
+ return derived + "_" + str(anonymous_counter)
def bindparam_string(self, name):
if self.positional:
def visit_alias(self, alias, asfrom=False, **kwargs):
if asfrom:
- return self.process(alias.original, asfrom=True, **kwargs) + " AS " + self.preparer.format_alias(alias, self._anonymize(alias.name))
+ return self.process(alias.original, asfrom=True, **kwargs) + " AS " + self.preparer.format_alias(alias, alias.name % self.anon_map)
else:
return self.process(alias.original, **kwargs)
not column.is_literal and \
column.table is not None and \
not isinstance(column.table, sql.Select):
- return _CompileLabel(column, column.name)
+ return _CompileLabel(column, sql._generated_label(column.name))
elif not isinstance(column, (sql._UnaryExpression, sql._TextClause, sql._BindParamClause)) and (not hasattr(column, 'name') or isinstance(column, sql._Function)):
return _CompileLabel(column, column.anon_label)
else:
# TODO: use UnaryExpression for this instead ?
modifier = _FunctionGenerator(group=False)
+class _generated_label(unicode):
+ """A unicode subclass used to identify dynamically generated names."""
+
def _clone(element):
return element._clone()
expressions and function calls.
"""
- return "{ANON %d %s}" % (id(self), getattr(self, 'name', 'anon'))
+ return _generated_label("%%(%d %s)s" % (id(self), getattr(self, 'name', 'anon')))
class ColumnCollection(util.OrderedProperties):
"""An ordered dictionary that stores a list of ColumnElement
"""
if unique:
- self.key = "{ANON %d %s}" % (id(self), key or 'param')
+ self.key = _generated_label("%%(%d %s)s" % (id(self), key or 'param'))
else:
- self.key = key or "{ANON %d param}" % id(self)
+ self.key = key or _generated_label("%%(%d param)s" % id(self))
self._orig_key = key or 'param'
self.unique = unique
self.value = value
def _clone(self):
c = ClauseElement._clone(self)
if self.unique:
- c.key = "{ANON %d %s}" % (id(c), c._orig_key or 'param')
+ c.key = _generated_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 = "{ANON %d %s}" % (id(self), self._orig_key or 'param')
+ self.key = _generated_label("%%(%d %s)s" % (id(self), self._orig_key or 'param'))
def _get_from_objects(self, **modifiers):
return []
if alias is None:
if self.original.named_with_column:
alias = getattr(self.original, 'name', None)
- alias = '{ANON %d %s}' % (id(self), alias or 'anon')
+ alias = _generated_label('%%(%d %s)s' % (id(self), alias or 'anon'))
self.name = alias
@property
def __init__(self, name, element, type_=None):
while isinstance(element, _Label):
element = element.element
- self.name = self.key = self._label = name or "{ANON %d %s}" % (id(self), getattr(element, 'name', 'anon'))
+ self.name = self.key = self._label = name or _generated_label("%%(%d %s)s" % (id(self), getattr(element, 'name', 'anon')))
self._element = element
self._type = type_
self.quote = element.quote
_label = label + "_" + str(counter)
counter += 1
label = _label
- return label
+ return _generated_label(label)
else:
return self.name
Column('c1', Integer, primary_key=True),
Column('c2', String(30)))
- @profiling.function_call_count(72, {'2.4': 42})
+ @profiling.function_call_count(68, {'2.4': 42})
def test_insert(self):
t1.insert().compile()
- @profiling.function_call_count(70, {'2.4': 45})
+ @profiling.function_call_count(68, {'2.4': 45})
def test_update(self):
t1.update().compile()
- @profiling.function_call_count(202, versions={'2.4':133})
+ @profiling.function_call_count(195, versions={'2.4':133})
def test_select(self):
s = select([t1], t1.c.c2==t2.c.c1)
s.compile()
def test_profile_2_insert(self):
self.test_baseline_2_insert()
- @profiling.function_call_count(4178, {'2.4': 2557})
+ @profiling.function_call_count(3858, {'2.4': 2557})
def test_profile_3_properties(self):
self.test_baseline_3_properties()
- @profiling.function_call_count(15869, {'2.4': 10549})
+ @profiling.function_call_count(14752, {'2.4': 10549})
def test_profile_4_expressions(self):
self.test_baseline_4_expressions()
def test_profile_5_aggregates(self):
self.test_baseline_5_aggregates()
- @profiling.function_call_count(2054, {'2.4': 1256})
+ @profiling.function_call_count(1904, {'2.4': 1256})
def test_profile_6_editing(self):
self.test_baseline_6_editing()
- @profiling.function_call_count(3276, {'2.4': 2198})
+ @profiling.function_call_count(3110, {'2.4': 2198})
def test_profile_7_multiview(self):
self.test_baseline_7_multiview()
def test_profile_2_insert(self):
self.test_baseline_2_insert()
- @profiling.function_call_count(7305)
+ @profiling.function_call_count(6765)
def test_profile_3_properties(self):
self.test_baseline_3_properties()
- @profiling.function_call_count(25760)
+ @profiling.function_call_count(23957)
def test_profile_4_expressions(self):
self.test_baseline_4_expressions()
@testing.requires.subqueries
def test_subquery(self):
- # this is the test that fails if the "max identifier length" is
- # shorter than the length of the actual columns created, because the
- # column names get truncated. if you try to separate "physical
- # columns" from "labels", and only truncate the labels, the
- # compiler.DefaultCompiler.visit_select() logic which auto-labels
- # columns in a subquery (for the purposes of sqlite compat) breaks the
- # code, since it is creating "labels" on the fly but not affecting
- # derived columns, which think they are still "physical"
q = table1.select(table1.c.this_is_the_primarykey_column == 4).alias('foo')
x = select([q])
print x.execute().fetchall()
print x.execute().fetchall()
+ def test_adjustable(self):
+
+ q = table1.select(table1.c.this_is_the_primarykey_column == 4).alias('foo')
+ x = select([q])
+
+ compile_dialect = default.DefaultDialect(label_length=10)
+ self.assert_compile(x, "SELECT foo.this_is_the_primarykey_column, foo.this_is_the_data_column FROM "
+ "(SELECT some_large_named_table.this_is_the_primarykey_column AS this_1, some_large_named_table.this_is_the_data_column "
+ "AS this_2 FROM some_large_named_table WHERE some_large_named_table.this_is_the_primarykey_column = :this_1) AS foo", dialect=compile_dialect)
+
+ compile_dialect = default.DefaultDialect(label_length=4)
+ self.assert_compile(x, "SELECT foo.this_is_the_primarykey_column, foo.this_is_the_data_column FROM "
+ "(SELECT some_large_named_table.this_is_the_primarykey_column AS _1, some_large_named_table.this_is_the_data_column AS _2 "
+ "FROM some_large_named_table WHERE some_large_named_table.this_is_the_primarykey_column = :_1) AS foo", dialect=compile_dialect)
+
+ q = table1.select(table1.c.this_is_the_primarykey_column == 4).alias()
+ x = select([q], use_labels=True)
+
+ compile_dialect = default.DefaultDialect(label_length=10)
+ self.assert_compile(x, "SELECT anon_1.this_is_the_primarykey_column AS anon_1, anon_1.this_is_the_data_column AS anon_2 FROM "
+ "(SELECT some_large_named_table.this_is_the_primarykey_column AS this_3, some_large_named_table.this_is_the_data_column AS this_4 "
+ "FROM some_large_named_table WHERE some_large_named_table.this_is_the_primarykey_column = :this_1) AS anon_1", dialect=compile_dialect)
+
+ compile_dialect = default.DefaultDialect(label_length=4)
+ self.assert_compile(x, "SELECT anon_1.this_is_the_primarykey_column AS _1, anon_1.this_is_the_data_column AS _2 FROM "
+ "(SELECT some_large_named_table.this_is_the_primarykey_column AS _3, some_large_named_table.this_is_the_data_column AS _4 "
+ "FROM some_large_named_table WHERE some_large_named_table.this_is_the_primarykey_column = :_1) AS anon_1", dialect=compile_dialect)
+
+
if __name__ == '__main__':
testenv.main()