From d64b09b15c703d7e100931818976822256f6beda Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 27 Jul 2013 17:05:01 -0400 Subject: - fix issue in join rewriting whereby we need to ensure the .key and .name are transferred correctly for when .key is present; tests have been enhanced to test this condition for render, result map construction, statement execution. [ticket:2790] --- lib/sqlalchemy/sql/compiler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index a5f545de9..72bebca3d 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1162,7 +1162,8 @@ class SQLCompiler(engine.Compiled): use_labels=True).alias() for c in selectable.c: - c._label = c._key_label = c.name + c._key_label = c.key + c._label = c.name translate_dict = dict( zip(right.element.c, selectable.c) ) @@ -1199,6 +1200,7 @@ class SQLCompiler(engine.Compiled): def _transform_result_map_for_nested_joins(self, select, transformed_select): inner_col = dict((c._key_label, c) for c in transformed_select.inner_columns) + d = dict( (inner_col[c._key_label], c) for c in select.inner_columns -- cgit v1.2.1 From 8b0f4d2a92c54cde9fefa8182bbd1bb503071d49 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 31 Jul 2013 18:42:58 -0400 Subject: - Fixed bug in common table expression system where if the CTE were used only as an ``alias()`` construct, it would not render using the WITH keyword. Also in 0.8.3, 0.7.11. [ticket:2783] --- lib/sqlalchemy/sql/compiler.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 72bebca3d..daed7c50f 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -961,12 +961,17 @@ class SQLCompiler(engine.Compiled): self.ctes_by_name[cte_name] = cte - if cte.cte_alias: - if isinstance(cte.cte_alias, sql._truncated_label): - cte_alias = self._truncated_identifier("alias", cte.cte_alias) - else: - cte_alias = cte.cte_alias - if not cte.cte_alias and cte not in self.ctes: + if cte._cte_alias is not None: + orig_cte = cte._cte_alias + if orig_cte not in self.ctes: + self.visit_cte(orig_cte) + cte_alias_name = cte._cte_alias.name + if isinstance(cte_alias_name, sql._truncated_label): + cte_alias_name = self._truncated_identifier("alias", cte_alias_name) + else: + orig_cte = cte + cte_alias_name = None + if not cte_alias_name and cte not in self.ctes: if cte.recursive: self.ctes_recursive = True text = self.preparer.format_alias(cte, cte_name) @@ -989,9 +994,10 @@ class SQLCompiler(engine.Compiled): self, asfrom=True, **kwargs ) self.ctes[cte] = text + if asfrom: - if cte.cte_alias: - text = self.preparer.format_alias(cte, cte_alias) + if cte_alias_name: + text = self.preparer.format_alias(cte, cte_alias_name) text += " AS " + cte_name else: return self.preparer.format_alias(cte, cte_name) -- cgit v1.2.1 From f6198d9abf453182f4b111e0579a7a4ef1614e79 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 12 Aug 2013 17:50:37 -0400 Subject: - A large refactoring of the ``sqlalchemy.sql`` package has reorganized the import structure of many core modules. ``sqlalchemy.schema`` and ``sqlalchemy.types`` remain in the top-level package, but are now just lists of names that pull from within ``sqlalchemy.sql``. Their implementations are now broken out among ``sqlalchemy.sql.type_api``, ``sqlalchemy.sql.sqltypes``, ``sqlalchemy.sql.schema`` and ``sqlalchemy.sql.ddl``, the last of which was moved from ``sqlalchemy.engine``. ``sqlalchemy.sql.expression`` is also a namespace now which pulls implementations mostly from ``sqlalchemy.sql.elements``, ``sqlalchemy.sql.selectable``, and ``sqlalchemy.sql.dml``. Most of the "factory" functions used to create SQL expression objects have been moved to classmethods or constructors, which are exposed in ``sqlalchemy.sql.expression`` using a programmatic system. Care has been taken such that all the original import namespaces remain intact and there should be no impact on any existing applications. The rationale here was to break out these very large modules into smaller ones, provide more manageable lists of function names, to greatly reduce "import cycles" and clarify the up-front importing of names, and to remove the need for redundant functions and documentation throughout the expression package. --- lib/sqlalchemy/sql/compiler.py | 216 ++++++++++++++++++++++++++++++----------- 1 file changed, 159 insertions(+), 57 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index daed7c50f..a6e6987c5 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -23,11 +23,9 @@ To generate user-defined SQL strings, see """ import re -import sys -from .. import schema, engine, util, exc, types -from . import ( - operators, functions, util as sql_util, visitors, expression as sql -) +from . import schema, sqltypes, operators, functions, \ + util as sql_util, visitors, elements, selectable +from .. import util, exc import decimal import itertools @@ -150,14 +148,118 @@ EXTRACT_MAP = { } COMPOUND_KEYWORDS = { - sql.CompoundSelect.UNION: 'UNION', - sql.CompoundSelect.UNION_ALL: 'UNION ALL', - sql.CompoundSelect.EXCEPT: 'EXCEPT', - sql.CompoundSelect.EXCEPT_ALL: 'EXCEPT ALL', - sql.CompoundSelect.INTERSECT: 'INTERSECT', - sql.CompoundSelect.INTERSECT_ALL: 'INTERSECT ALL' + selectable.CompoundSelect.UNION: 'UNION', + selectable.CompoundSelect.UNION_ALL: 'UNION ALL', + selectable.CompoundSelect.EXCEPT: 'EXCEPT', + selectable.CompoundSelect.EXCEPT_ALL: 'EXCEPT ALL', + selectable.CompoundSelect.INTERSECT: 'INTERSECT', + selectable.CompoundSelect.INTERSECT_ALL: 'INTERSECT ALL' } +class Compiled(object): + """Represent a compiled SQL or DDL expression. + + The ``__str__`` method of the ``Compiled`` object should produce + the actual text of the statement. ``Compiled`` objects are + specific to their underlying database dialect, and also may + or may not be specific to the columns referenced within a + particular set of bind parameters. In no case should the + ``Compiled`` object be dependent on the actual values of those + bind parameters, even though it may reference those values as + defaults. + """ + + def __init__(self, dialect, statement, bind=None, + compile_kwargs=util.immutabledict()): + """Construct a new ``Compiled`` object. + + :param dialect: ``Dialect`` to compile against. + + :param statement: ``ClauseElement`` to be compiled. + + :param bind: Optional Engine or Connection to compile this + statement against. + + :param compile_kwargs: additional kwargs that will be + passed to the initial call to :meth:`.Compiled.process`. + + .. versionadded:: 0.8 + + """ + + self.dialect = dialect + self.bind = bind + if statement is not None: + self.statement = statement + self.can_execute = statement.supports_execution + self.string = self.process(self.statement, **compile_kwargs) + + @util.deprecated("0.7", ":class:`.Compiled` objects now compile " + "within the constructor.") + def compile(self): + """Produce the internal string representation of this element.""" + pass + + @property + def sql_compiler(self): + """Return a Compiled that is capable of processing SQL expressions. + + If this compiler is one, it would likely just return 'self'. + + """ + + raise NotImplementedError() + + def process(self, obj, **kwargs): + return obj._compiler_dispatch(self, **kwargs) + + def __str__(self): + """Return the string text of the generated SQL or DDL.""" + + return self.string or '' + + def construct_params(self, params=None): + """Return the bind params for this compiled object. + + :param params: a dict of string/object pairs whose values will + override bind values compiled in to the + statement. + """ + + raise NotImplementedError() + + @property + def params(self): + """Return the bind params for this compiled object.""" + return self.construct_params() + + def execute(self, *multiparams, **params): + """Execute this compiled object.""" + + e = self.bind + if e is None: + raise exc.UnboundExecutionError( + "This Compiled object is not bound to any Engine " + "or Connection.") + return e._execute_compiled(self, multiparams, params) + + def scalar(self, *multiparams, **params): + """Execute this compiled object and return the result's + scalar value.""" + + return self.execute(*multiparams, **params).scalar() + + +class TypeCompiler(object): + """Produces DDL specification for TypeEngine objects.""" + + def __init__(self, dialect): + self.dialect = dialect + + def process(self, type_): + return type_._compiler_dispatch(self) + + class _CompileLabel(visitors.Visitable): """lightweight label object which acts as an expression.Label.""" @@ -183,7 +285,7 @@ class _CompileLabel(visitors.Visitable): return self.element.quote -class SQLCompiler(engine.Compiled): +class SQLCompiler(Compiled): """Default implementation of Compiled. Compiles ClauseElements into SQL strings. Uses a similar visit @@ -284,7 +386,7 @@ class SQLCompiler(engine.Compiled): # a map which tracks "truncated" names based on # dialect.label_length or dialect.max_identifier_length self.truncated_names = {} - engine.Compiled.__init__(self, dialect, statement, **kwargs) + Compiled.__init__(self, dialect, statement, **kwargs) if self.positional and dialect.paramstyle == 'numeric': self._apply_numbered_params() @@ -397,7 +499,7 @@ class SQLCompiler(engine.Compiled): render_label_only = render_label_as_label is label if render_label_only or render_label_with_as: - if isinstance(label.name, sql._truncated_label): + if isinstance(label.name, elements._truncated_label): labelname = self._truncated_identifier("colident", label.name) else: labelname = label.name @@ -432,7 +534,7 @@ class SQLCompiler(engine.Compiled): "its 'name' is assigned.") is_literal = column.is_literal - if not is_literal and isinstance(name, sql._truncated_label): + if not is_literal and isinstance(name, elements._truncated_label): name = self._truncated_identifier("colident", name) if add_to_result_map is not None: @@ -459,7 +561,7 @@ class SQLCompiler(engine.Compiled): else: schema_prefix = '' tablename = table.name - if isinstance(tablename, sql._truncated_label): + if isinstance(tablename, elements._truncated_label): tablename = self._truncated_identifier("alias", tablename) return schema_prefix + \ @@ -687,8 +789,8 @@ class SQLCompiler(engine.Compiled): def visit_binary(self, binary, **kw): # don't allow "? = ?" to render if self.ansi_bind_rules and \ - isinstance(binary.left, sql.BindParameter) and \ - isinstance(binary.right, sql.BindParameter): + isinstance(binary.left, elements.BindParameter) and \ + isinstance(binary.right, elements.BindParameter): kw['literal_binds'] = True operator = binary.operator @@ -728,7 +830,7 @@ class SQLCompiler(engine.Compiled): @util.memoized_property def _like_percent_literal(self): - return sql.literal_column("'%'", type_=types.String()) + return elements.literal_column("'%'", type_=sqltypes.String()) def visit_contains_op_binary(self, binary, operator, **kw): binary = binary._clone() @@ -888,7 +990,7 @@ class SQLCompiler(engine.Compiled): return self.bind_names[bindparam] bind_name = bindparam.key - if isinstance(bind_name, sql._truncated_label): + if isinstance(bind_name, elements._truncated_label): bind_name = self._truncated_identifier("bindparam", bind_name) # add to bind_names for translation @@ -937,7 +1039,7 @@ class SQLCompiler(engine.Compiled): if self.positional: kwargs['positional_names'] = self.cte_positional - if isinstance(cte.name, sql._truncated_label): + if isinstance(cte.name, elements._truncated_label): cte_name = self._truncated_identifier("alias", cte.name) else: cte_name = cte.name @@ -966,7 +1068,7 @@ class SQLCompiler(engine.Compiled): if orig_cte not in self.ctes: self.visit_cte(orig_cte) cte_alias_name = cte._cte_alias.name - if isinstance(cte_alias_name, sql._truncated_label): + if isinstance(cte_alias_name, elements._truncated_label): cte_alias_name = self._truncated_identifier("alias", cte_alias_name) else: orig_cte = cte @@ -976,9 +1078,9 @@ class SQLCompiler(engine.Compiled): self.ctes_recursive = True text = self.preparer.format_alias(cte, cte_name) if cte.recursive: - if isinstance(cte.original, sql.Select): + if isinstance(cte.original, selectable.Select): col_source = cte.original - elif isinstance(cte.original, sql.CompoundSelect): + elif isinstance(cte.original, selectable.CompoundSelect): col_source = cte.original.selects[0] else: assert False @@ -1007,7 +1109,7 @@ class SQLCompiler(engine.Compiled): iscrud=False, fromhints=None, **kwargs): if asfrom or ashint: - if isinstance(alias.name, sql._truncated_label): + if isinstance(alias.name, elements._truncated_label): alias_name = self._truncated_identifier("alias", alias.name) else: alias_name = alias.name @@ -1065,7 +1167,7 @@ class SQLCompiler(engine.Compiled): if not within_columns_clause: result_expr = col_expr - elif isinstance(column, sql.Label): + elif isinstance(column, elements.Label): if col_expr is not column: result_expr = _CompileLabel( col_expr, @@ -1084,23 +1186,23 @@ class SQLCompiler(engine.Compiled): elif \ asfrom and \ - isinstance(column, sql.ColumnClause) and \ + isinstance(column, elements.ColumnClause) and \ not column.is_literal and \ column.table is not None and \ - not isinstance(column.table, sql.Select): + not isinstance(column.table, selectable.Select): result_expr = _CompileLabel(col_expr, - sql._as_truncated(column.name), + elements._as_truncated(column.name), alt_names=(column.key,)) elif not isinstance(column, - (sql.UnaryExpression, sql.TextClause)) \ + (elements.UnaryExpression, elements.TextClause)) \ and (not hasattr(column, 'name') or \ - isinstance(column, sql.Function)): + isinstance(column, functions.Function)): result_expr = _CompileLabel(col_expr, column.anon_label) elif col_expr is not column: # TODO: are we sure "column" has a .name and .key here ? - # assert isinstance(column, sql.ColumnClause) + # assert isinstance(column, elements.ColumnClause) result_expr = _CompileLabel(col_expr, - sql._as_truncated(column.name), + elements._as_truncated(column.name), alt_names=(column.key,)) else: result_expr = col_expr @@ -1143,8 +1245,8 @@ class SQLCompiler(engine.Compiled): # as this whole system won't work for custom Join/Select # subclasses where compilation routines # call down to compiler.visit_join(), compiler.visit_select() - join_name = sql.Join.__visit_name__ - select_name = sql.Select.__visit_name__ + join_name = selectable.Join.__visit_name__ + select_name = selectable.Select.__visit_name__ def visit(element, **kw): if element in column_translate[-1]: @@ -1156,25 +1258,25 @@ class SQLCompiler(engine.Compiled): newelem = cloned[element] = element._clone() if newelem.__visit_name__ is join_name and \ - isinstance(newelem.right, sql.FromGrouping): + isinstance(newelem.right, selectable.FromGrouping): newelem._reset_exported() newelem.left = visit(newelem.left, **kw) right = visit(newelem.right, **kw) - selectable = sql.select( + selectable_ = selectable.Select( [right.element], use_labels=True).alias() - for c in selectable.c: + for c in selectable_.c: c._key_label = c.key c._label = c.name translate_dict = dict( - zip(right.element.c, selectable.c) + zip(right.element.c, selectable_.c) ) - translate_dict[right.element.left] = selectable - translate_dict[right.element.right] = selectable + translate_dict[right.element.left] = selectable_ + translate_dict[right.element.right] = selectable_ # propagate translations that we've gained # from nested visit(newelem.right) outwards @@ -1190,7 +1292,7 @@ class SQLCompiler(engine.Compiled): column_translate[-1].update(translate_dict) - newelem.right = selectable + newelem.right = selectable_ newelem.onclause = visit(newelem.onclause, **kw) elif newelem.__visit_name__ is select_name: column_translate.append({}) @@ -1299,7 +1401,7 @@ class SQLCompiler(engine.Compiled): explicit_correlate_froms=correlate_froms, implicit_correlate_froms=asfrom_froms) - new_correlate_froms = set(sql._from_objects(*froms)) + new_correlate_froms = set(selectable._from_objects(*froms)) all_correlate_froms = new_correlate_froms.union(correlate_froms) new_entry = { @@ -1461,11 +1563,11 @@ class SQLCompiler(engine.Compiled): def limit_clause(self, select): text = "" if select._limit is not None: - text += "\n LIMIT " + self.process(sql.literal(select._limit)) + text += "\n LIMIT " + self.process(elements.literal(select._limit)) if select._offset is not None: if select._limit is None: text += "\n LIMIT -1" - text += " OFFSET " + self.process(sql.literal(select._offset)) + text += " OFFSET " + self.process(elements.literal(select._offset)) return text def visit_table(self, table, asfrom=False, iscrud=False, ashint=False, @@ -1692,7 +1794,7 @@ class SQLCompiler(engine.Compiled): def _create_crud_bind_param(self, col, value, required=False, name=None): if name is None: name = col.key - bindparam = sql.bindparam(name, value, + bindparam = elements.BindParameter(name, value, type_=col.type, required=required, quote=col.quote) bindparam._is_crud = True @@ -1732,7 +1834,7 @@ class SQLCompiler(engine.Compiled): if self.column_keys is None: parameters = {} else: - parameters = dict((sql._column_as_key(key), REQUIRED) + parameters = dict((elements._column_as_key(key), REQUIRED) for key in self.column_keys if not stmt_parameters or key not in stmt_parameters) @@ -1742,15 +1844,15 @@ class SQLCompiler(engine.Compiled): if stmt_parameters is not None: for k, v in stmt_parameters.items(): - colkey = sql._column_as_key(k) + colkey = elements._column_as_key(k) if colkey is not None: parameters.setdefault(colkey, v) else: # a non-Column expression on the left side; # add it to values() in an "as-is" state, # coercing right side to bound param - if sql._is_literal(v): - v = self.process(sql.bindparam(None, v, type_=k.type)) + if elements._is_literal(v): + v = self.process(elements.BindParameter(None, v, type_=k.type)) else: v = self.process(v.self_group()) @@ -1771,7 +1873,7 @@ class SQLCompiler(engine.Compiled): # statements if extra_tables and stmt_parameters: normalized_params = dict( - (sql._clause_element_as_expr(c), param) + (elements._clause_element_as_expr(c), param) for c, param in stmt_parameters.items() ) assert self.isupdate @@ -1782,7 +1884,7 @@ class SQLCompiler(engine.Compiled): affected_tables.add(t) check_columns[c.key] = c value = normalized_params[c] - if sql._is_literal(value): + if elements._is_literal(value): value = self._create_crud_bind_param( c, value, required=value is REQUIRED) else: @@ -1816,7 +1918,7 @@ class SQLCompiler(engine.Compiled): for c in stmt.table.columns: if c.key in parameters and c.key not in check_columns: value = parameters.pop(c.key) - if sql._is_literal(value): + if elements._is_literal(value): value = self._create_crud_bind_param( c, value, required=value is REQUIRED, name=c.key @@ -1918,7 +2020,7 @@ class SQLCompiler(engine.Compiled): if parameters and stmt_parameters: check = set(parameters).intersection( - sql._column_as_key(k) for k in stmt.parameters + elements._column_as_key(k) for k in stmt.parameters ).difference(check_columns) if check: raise exc.CompileError( @@ -2013,7 +2115,7 @@ class SQLCompiler(engine.Compiled): self.preparer.format_savepoint(savepoint_stmt) -class DDLCompiler(engine.Compiled): +class DDLCompiler(Compiled): @util.memoized_property def sql_compiler(self): @@ -2183,7 +2285,7 @@ class DDLCompiler(engine.Compiled): schema_name = None ident = index.name - if isinstance(ident, sql._truncated_label): + if isinstance(ident, elements._truncated_label): max_ = self.dialect.max_index_name_length or \ self.dialect.max_identifier_length if len(ident) > max_: @@ -2343,7 +2445,7 @@ class DDLCompiler(engine.Compiled): return text -class GenericTypeCompiler(engine.TypeCompiler): +class GenericTypeCompiler(TypeCompiler): def visit_FLOAT(self, type_): return "FLOAT" -- cgit v1.2.1 From 236db85f96e84fe099e63182bc4e67ceb65bd0d0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 18 Aug 2013 14:46:04 -0400 Subject: Fixed regression dating back to 0.7.9 whereby the name of a CTE might not be properly quoted if it was referred to in multiple FROM clauses. Also in 0.8.3, 0.7.11. [ticket:2801] --- lib/sqlalchemy/sql/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index a6e6987c5..dc3d55039 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1049,7 +1049,7 @@ class SQLCompiler(Compiled): # we've generated a same-named CTE that we are enclosed in, # or this is the same CTE. just return the name. if cte in existing_cte._restates or cte is existing_cte: - return cte_name + return self.preparer.format_alias(cte, cte_name) elif existing_cte in cte._restates: # we've generated a same-named CTE that is # enclosed in us - we take precedence, so -- cgit v1.2.1 From e9b2e33f15cd74978ca858e768e9c44abb1d00f3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 24 Aug 2013 13:55:14 -0400 Subject: - The :class:`.CreateColumn` construct can be appled to a custom compilation rule which allows skipping of columns, by producing a rule that returns ``None``. Also in 0.8.3. --- lib/sqlalchemy/sql/compiler.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index dc3d55039..c56ca1ae4 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2178,11 +2178,13 @@ class DDLCompiler(Compiled): for create_column in create.columns: column = create_column.element try: - text += separator - separator = ", \n" - text += "\t" + self.process(create_column, + processed = self.process(create_column, first_pk=column.primary_key and not first_pk) + if processed is not None: + text += separator + separator = ", \n" + text += "\t" + processed if column.primary_key: first_pk = True except exc.CompileError as ce: -- cgit v1.2.1 From 2452c49cc4d2244d0efef78e051eb65f79b7c712 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 25 Aug 2013 12:28:47 -0400 Subject: added "system=True" to Column, so that we generally don't have to bother with CreateColumn rules --- lib/sqlalchemy/sql/compiler.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index c56ca1ae4..6370b1227 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2205,6 +2205,9 @@ class DDLCompiler(Compiled): def visit_create_column(self, create, first_pk=False): column = create.element + if column.system: + return None + text = self.get_column_specification( column, first_pk=first_pk -- cgit v1.2.1 From d6ce68727f8ad4c77cc64ac6bbc5fc17ecd2b8e3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 25 Aug 2013 14:03:54 -0400 Subject: - The ``version_id_generator`` parameter of ``Mapper`` can now be specified to rely upon server generated version identifiers, using triggers or other database-provided versioning features, by passing the value ``False``. The ORM will use RETURNING when available to immediately load the new version identifier, else it will emit a second SELECT. [ticket:2793] - The ``eager_defaults`` flag of :class:`.Mapper` will now allow the newly generated default values to be fetched using an inline RETURNING clause, rather than a second SELECT statement, for backends that support RETURNING. - Added a new variant to :meth:`.ValuesBase.returning` called :meth:`.ValuesBase.return_defaults`; this allows arbitrary columns to be added to the RETURNING clause of the statement without interfering with the compilers usual "implicit returning" feature, which is used to efficiently fetch newly generated primary key values. For supporting backends, a dictionary of all fetched values is present at :attr:`.ResultProxy.returned_defaults`. - add a glossary entry for RETURNING - add documentation for version id generation, [ticket:867] --- lib/sqlalchemy/sql/compiler.py | 59 +++++++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 6370b1227..5d05cbc29 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1761,11 +1761,12 @@ class SQLCompiler(Compiled): '=' + c[1] for c in colparams ) - if update_stmt._returning: - self.returning = update_stmt._returning + if self.returning or update_stmt._returning: + if not self.returning: + self.returning = update_stmt._returning if self.returning_precedes_values: text += " " + self.returning_clause( - update_stmt, update_stmt._returning) + update_stmt, self.returning) if extra_froms: extra_from_text = self.update_from_clause( @@ -1785,7 +1786,7 @@ class SQLCompiler(Compiled): if self.returning and not self.returning_precedes_values: text += " " + self.returning_clause( - update_stmt, update_stmt._returning) + update_stmt, self.returning) self.stack.pop(-1) @@ -1866,6 +1867,19 @@ class SQLCompiler(Compiled): self.dialect.implicit_returning and \ stmt.table.implicit_returning + if self.isinsert: + implicit_return_defaults = implicit_returning and stmt._return_defaults + elif self.isupdate: + implicit_return_defaults = self.dialect.implicit_returning and \ + stmt.table.implicit_returning and \ + stmt._return_defaults + + if implicit_return_defaults: + if stmt._return_defaults is True: + implicit_return_defaults = set(stmt.table.c) + else: + implicit_return_defaults = set(stmt._return_defaults) + postfetch_lastrowid = need_pks and self.dialect.postfetch_lastrowid check_columns = {} @@ -1928,6 +1942,10 @@ class SQLCompiler(Compiled): elif c.primary_key and implicit_returning: self.returning.append(c) value = self.process(value.self_group()) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + value = self.process(value.self_group()) else: self.postfetch.append(c) value = self.process(value.self_group()) @@ -1984,14 +2002,20 @@ class SQLCompiler(Compiled): not self.dialect.sequences_optional): proc = self.process(c.default) values.append((c, proc)) - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: self.postfetch.append(c) elif c.default.is_clause_element: values.append( (c, self.process(c.default.arg.self_group())) ) - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: # dont add primary key column to postfetch self.postfetch.append(c) else: @@ -2000,8 +2024,14 @@ class SQLCompiler(Compiled): ) self.prefetch.append(c) elif c.server_default is not None: - if not c.primary_key: + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + elif not c.primary_key: self.postfetch.append(c) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) elif self.isupdate: if c.onupdate is not None and not c.onupdate.is_sequence: @@ -2009,14 +2039,25 @@ class SQLCompiler(Compiled): values.append( (c, self.process(c.onupdate.arg.self_group())) ) - self.postfetch.append(c) + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + else: + self.postfetch.append(c) else: values.append( (c, self._create_crud_bind_param(c, None)) ) self.prefetch.append(c) elif c.server_onupdate is not None: - self.postfetch.append(c) + if implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + else: + self.postfetch.append(c) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) if parameters and stmt_parameters: check = set(parameters).intersection( -- cgit v1.2.1 From 031ef0807838842a827135dbace760da7aec215e Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 27 Aug 2013 20:43:22 -0400 Subject: - 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] --- lib/sqlalchemy/sql/compiler.py | 99 ++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 53 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') 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), ) -- cgit v1.2.1 From 03671e9ee4c13f4d7e98ec4a5076fd9c9979e44d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Wed, 25 Sep 2013 10:29:52 -0700 Subject: Replace a big loop + dict lookup in Connection.execute() with a simple visitor pattern --- lib/sqlalchemy/sql/compiler.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 8e3cf1729..f81886240 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -200,6 +200,9 @@ class Compiled(object): """Produce the internal string representation of this element.""" pass + def _execute_on_connection(self, connection, multiparams, params): + return connection._execute_compiled(self, multiparams, params) + @property def sql_compiler(self): """Return a Compiled that is capable of processing SQL expressions. -- cgit v1.2.1 From 9bc9d5c1068be878118202259add3c2e1bcec0cb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 12 Oct 2013 20:04:55 -0400 Subject: - Fixed bug in default compiler plus those of postgresql, mysql, and mssql to ensure that any literal SQL expression values are rendered directly as literals, instead of as bound parameters, within a CREATE INDEX statement. [ticket:2742] - don't need expression_as_ddl(); literal_binds and include_table take care of this functionality. --- lib/sqlalchemy/sql/compiler.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index f81886240..9f2abd6f5 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2305,7 +2305,7 @@ class DDLCompiler(Compiled): use_schema=include_table_schema), ', '.join( self.sql_compiler.process(expr, - include_table=False) for + include_table=False, literal_binds=True) for expr in index.expressions) ) return text @@ -2392,8 +2392,9 @@ class DDLCompiler(Compiled): if constraint.name is not None: text += "CONSTRAINT %s " % \ self.preparer.format_constraint(constraint) - sqltext = sql_util.expression_as_ddl(constraint.sqltext) - text += "CHECK (%s)" % self.sql_compiler.process(sqltext) + text += "CHECK (%s)" % self.sql_compiler.process(constraint.sqltext, + include_table=False, + literal_binds=True) text += self.define_constraint_deferrability(constraint) return text -- cgit v1.2.1 From dbde70a3a23505ab462da3da8639ee22691a0788 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 14 Oct 2013 10:56:11 -0400 Subject: workaround for #2838 here. still need to figure out why an ENUM test is suddenly hitting this. --- lib/sqlalchemy/sql/compiler.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 9f2abd6f5..22906af54 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -978,6 +978,13 @@ class SQLCompiler(Compiled): return repr(value) elif isinstance(value, decimal.Decimal): return str(value) + elif isinstance(value, util.binary_type): + # only would occur on py3k b.c. on 2k the string_types + # directive above catches this. + # see #2838 + value = value.decode(self.dialect.encoding).replace("'", "''") + return "'%s'" % value + else: raise NotImplementedError( "Don't know how to literal-quote value %r" % value) -- cgit v1.2.1 From 4663ec98b226a7d495846f0d89c646110705bb30 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 20 Oct 2013 16:59:56 -0400 Subject: - The typing system now handles the task of rendering "literal bind" values, e.g. values that are normally bound parameters but due to context must be rendered as strings, typically within DDL constructs such as CHECK constraints and indexes (note that "literal bind" values become used by DDL as of :ticket:`2742`). A new method :meth:`.TypeEngine.literal_processor` serves as the base, and :meth:`.TypeDecorator.process_literal_param` is added to allow wrapping of a native literal rendering method. [ticket:2838] - enhance _get_colparams so that we can send flags like literal_binds into INSERT statements - add support in PG for inspecting standard_conforming_strings - add a new series of roundtrip tests based on INSERT of literal plus SELECT for basic literal rendering in dialect suite --- lib/sqlalchemy/sql/compiler.py | 87 ++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 46 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 22906af54..5c7a29f99 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -827,7 +827,7 @@ class SQLCompiler(Compiled): @util.memoized_property def _like_percent_literal(self): - return elements.literal_column("'%'", type_=sqltypes.String()) + return elements.literal_column("'%'", type_=sqltypes.STRINGTYPE) def visit_contains_op_binary(self, binary, operator, **kw): binary = binary._clone() @@ -871,39 +871,49 @@ class SQLCompiler(Compiled): def visit_like_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) + + # TODO: use ternary here, not "and"/ "or" return '%s LIKE %s' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def visit_notlike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return '%s NOT LIKE %s' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def visit_ilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return 'lower(%s) LIKE lower(%s)' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def visit_notilike_op_binary(self, binary, operator, **kw): escape = binary.modifiers.get("escape", None) return 'lower(%s) NOT LIKE lower(%s)' % ( binary.left._compiler_dispatch(self, **kw), binary.right._compiler_dispatch(self, **kw)) \ - + (escape and - (' ESCAPE ' + self.render_literal_value(escape, None)) - or '') + + ( + ' ESCAPE ' + + self.render_literal_value(escape, sqltypes.STRINGTYPE) + if escape else '' + ) def visit_bindparam(self, bindparam, within_columns_clause=False, literal_binds=False, @@ -954,9 +964,6 @@ class SQLCompiler(Compiled): def render_literal_bindparam(self, bindparam, **kw): value = bindparam.value - processor = bindparam.type._cached_bind_processor(self.dialect) - if processor: - value = processor(value) return self.render_literal_value(value, bindparam.type) def render_literal_value(self, value, type_): @@ -969,22 +976,10 @@ class SQLCompiler(Compiled): of the DBAPI. """ - if isinstance(value, util.string_types): - value = value.replace("'", "''") - return "'%s'" % value - elif value is None: - return "NULL" - elif isinstance(value, (float, ) + util.int_types): - return repr(value) - elif isinstance(value, decimal.Decimal): - return str(value) - elif isinstance(value, util.binary_type): - # only would occur on py3k b.c. on 2k the string_types - # directive above catches this. - # see #2838 - value = value.decode(self.dialect.encoding).replace("'", "''") - return "'%s'" % value + processor = type_._cached_literal_processor(self.dialect) + if processor: + return processor(value) else: raise NotImplementedError( "Don't know how to literal-quote value %r" % value) @@ -1599,7 +1594,7 @@ class SQLCompiler(Compiled): def visit_insert(self, insert_stmt, **kw): self.isinsert = True - colparams = self._get_colparams(insert_stmt) + colparams = self._get_colparams(insert_stmt, **kw) if not colparams and \ not self.dialect.supports_default_values and \ @@ -1732,7 +1727,7 @@ class SQLCompiler(Compiled): table_text = self.update_tables_clause(update_stmt, update_stmt.table, extra_froms, **kw) - colparams = self._get_colparams(update_stmt, extra_froms) + colparams = self._get_colparams(update_stmt, extra_froms, **kw) if update_stmt._hints: dialect_hints = dict([ @@ -1801,7 +1796,7 @@ class SQLCompiler(Compiled): bindparam._is_crud = True return bindparam._compiler_dispatch(self) - def _get_colparams(self, stmt, extra_tables=None): + def _get_colparams(self, stmt, extra_tables=None, **kw): """create a set of tuples representing column/string pairs for use in an INSERT or UPDATE statement. @@ -1853,9 +1848,9 @@ class SQLCompiler(Compiled): # add it to values() in an "as-is" state, # coercing right side to bound param if elements._is_literal(v): - v = self.process(elements.BindParameter(None, v, type_=k.type)) + v = self.process(elements.BindParameter(None, v, type_=k.type), **kw) else: - v = self.process(v.self_group()) + v = self.process(v.self_group(), **kw) values.append((k, v)) @@ -1903,7 +1898,7 @@ class SQLCompiler(Compiled): c, value, required=value is REQUIRED) else: self.postfetch.append(c) - value = self.process(value.self_group()) + value = self.process(value.self_group(), **kw) values.append((c, value)) # determine tables which are actually # to be updated - process onupdate and @@ -1915,7 +1910,7 @@ class SQLCompiler(Compiled): elif c.onupdate is not None and not c.onupdate.is_sequence: if c.onupdate.is_clause_element: values.append( - (c, self.process(c.onupdate.arg.self_group())) + (c, self.process(c.onupdate.arg.self_group(), **kw)) ) self.postfetch.append(c) else: @@ -1941,14 +1936,14 @@ class SQLCompiler(Compiled): ) elif c.primary_key and implicit_returning: self.returning.append(c) - value = self.process(value.self_group()) + value = self.process(value.self_group(), **kw) elif implicit_return_defaults and \ c in implicit_return_defaults: self.returning.append(c) - value = self.process(value.self_group()) + value = self.process(value.self_group(), **kw) else: self.postfetch.append(c) - value = self.process(value.self_group()) + value = self.process(value.self_group(), **kw) values.append((c, value)) elif self.isinsert: @@ -1966,13 +1961,13 @@ class SQLCompiler(Compiled): if self.dialect.supports_sequences and \ (not c.default.optional or \ not self.dialect.sequences_optional): - proc = self.process(c.default) + proc = self.process(c.default, **kw) values.append((c, proc)) self.returning.append(c) elif c.default.is_clause_element: values.append( (c, - self.process(c.default.arg.self_group())) + self.process(c.default.arg.self_group(), **kw)) ) self.returning.append(c) else: @@ -2000,7 +1995,7 @@ class SQLCompiler(Compiled): if self.dialect.supports_sequences and \ (not c.default.optional or \ not self.dialect.sequences_optional): - proc = self.process(c.default) + proc = self.process(c.default, **kw) values.append((c, proc)) if implicit_return_defaults and \ c in implicit_return_defaults: @@ -2009,7 +2004,7 @@ class SQLCompiler(Compiled): self.postfetch.append(c) elif c.default.is_clause_element: values.append( - (c, self.process(c.default.arg.self_group())) + (c, self.process(c.default.arg.self_group(), **kw)) ) if implicit_return_defaults and \ @@ -2037,7 +2032,7 @@ class SQLCompiler(Compiled): if c.onupdate is not None and not c.onupdate.is_sequence: if c.onupdate.is_clause_element: values.append( - (c, self.process(c.onupdate.arg.self_group())) + (c, self.process(c.onupdate.arg.self_group(), **kw)) ) if implicit_return_defaults and \ c in implicit_return_defaults: -- cgit v1.2.1 From 9caa92b96fef0b3e6d9d280407bc6a092a77b584 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 21 Oct 2013 16:49:46 -0400 Subject: - A :func:`.bindparam` construct with a "null" type (e.g. no type specified) is now copied when used in a typed expression, and the new copy is assigned the actual type of the compared column. Previously, this logic would occur on the given :func:`.bindparam` in place. Additionally, a similar process now occurs for :func:`.bindparam` constructs passed to :meth:`.ValuesBase.values` for a :class:`.Insert` or :class:`.Update` construct. [ticket:2850] --- lib/sqlalchemy/sql/compiler.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 5c7a29f99..f526203ac 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1934,16 +1934,22 @@ class SQLCompiler(Compiled): if not stmt._has_multi_parameters else "%s_0" % c.key ) - elif c.primary_key and implicit_returning: - self.returning.append(c) - value = self.process(value.self_group(), **kw) - elif implicit_return_defaults and \ - c in implicit_return_defaults: - self.returning.append(c) - value = self.process(value.self_group(), **kw) else: - self.postfetch.append(c) - value = self.process(value.self_group(), **kw) + if isinstance(value, elements.BindParameter) and \ + value.type._isnull: + value = value._clone() + value.type = c.type + + if c.primary_key and implicit_returning: + self.returning.append(c) + value = self.process(value.self_group(), **kw) + elif implicit_return_defaults and \ + c in implicit_return_defaults: + self.returning.append(c) + value = self.process(value.self_group(), **kw) + else: + self.postfetch.append(c) + value = self.process(value.self_group(), **kw) values.append((c, value)) elif self.isinsert: -- cgit v1.2.1 From f035b6e0a41238d092ea2ddd10fdd5de298ff789 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 23 Oct 2013 17:41:55 -0400 Subject: An overhaul of expression handling for special symbols particularly with conjunctions, e.g. ``None`` :func:`.expression.null` :func:`.expression.true` :func:`.expression.false`, including consistency in rendering NULL in conjunctions, "short-circuiting" of :func:`.and_` and :func:`.or_` expressions which contain boolean constants, and rendering of boolean constants and expressions as compared to "1" or "0" for backends that don't feature ``true``/``false`` constants. [ticket:2804] --- lib/sqlalchemy/sql/compiler.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index f526203ac..2bf7d3f4a 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -113,6 +113,7 @@ OPERATORS = { operators.asc_op: ' ASC', operators.nullsfirst_op: ' NULLS FIRST', operators.nullslast_op: ' NULLS LAST', + } FUNCTIONS = { @@ -608,10 +609,16 @@ class SQLCompiler(Compiled): return 'NULL' def visit_true(self, expr, **kw): - return 'true' + if self.dialect.supports_native_boolean: + return 'true' + else: + return "1" def visit_false(self, expr, **kw): - return 'false' + if self.dialect.supports_native_boolean: + return 'false' + else: + return "0" def visit_clauselist(self, clauselist, order_by_select=None, **kw): if order_by_select is not None: @@ -783,6 +790,18 @@ class SQLCompiler(Compiled): raise exc.CompileError( "Unary expression has no operator or modifier") + def visit_istrue_unary_operator(self, element, operator, **kw): + if self.dialect.supports_native_boolean: + return self.process(element.element, **kw) + else: + return "%s = 1" % self.process(element.element, **kw) + + def visit_isfalse_unary_operator(self, element, operator, **kw): + if self.dialect.supports_native_boolean: + return "NOT %s" % self.process(element.element, **kw) + else: + return "%s = 0" % self.process(element.element, **kw) + def visit_binary(self, binary, **kw): # don't allow "? = ?" to render if self.ansi_bind_rules and \ -- cgit v1.2.1 From 5d0e84434f116460d64d3078f650d9c89004f2a9 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 1 Nov 2013 15:24:43 -0400 Subject: - Fixed a regression introduced by the join rewriting feature of :ticket:`2369` and :ticket:`2587` where a nested join with one side already an aliased select would fail to translate the ON clause on the outside correctly; in the ORM this could be seen when using a SELECT statement as a "secondary" table. [ticket:2858] --- lib/sqlalchemy/sql/compiler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 2bf7d3f4a..4f3dbba36 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1289,9 +1289,11 @@ class SQLCompiler(Compiled): for c in selectable_.c: c._key_label = c.key c._label = c.name + translate_dict = dict( - zip(right.element.c, selectable_.c) - ) + zip(newelem.right.element.c, selectable_.c) + ) + translate_dict[right.element.left] = selectable_ translate_dict[right.element.right] = selectable_ @@ -1310,6 +1312,7 @@ class SQLCompiler(Compiled): column_translate[-1].update(translate_dict) newelem.right = selectable_ + newelem.onclause = visit(newelem.onclause, **kw) elif newelem.__visit_name__ is select_name: column_translate.append({}) -- cgit v1.2.1 From 71c45937f9adbb64482fffcda75f8fe4d063e027 Mon Sep 17 00:00:00 2001 From: Mario Lassnig Date: Tue, 12 Nov 2013 23:08:51 +0100 Subject: add psql FOR UPDATE OF functionality --- lib/sqlalchemy/sql/compiler.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 4f3dbba36..51ec0d9eb 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1571,6 +1571,8 @@ class SQLCompiler(Compiled): def for_update_clause(self, select): if select.for_update: + if select.for_update_of is not None: + return " FOR UPDATE OF " + select.for_update_of return " FOR UPDATE" else: return "" -- cgit v1.2.1 From 631eb841084a50518af90e2e728bd1efd31228f7 Mon Sep 17 00:00:00 2001 From: Vraj Mohan Date: Mon, 11 Nov 2013 15:04:52 -0500 Subject: Fix cross references --- lib/sqlalchemy/sql/compiler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 4f3dbba36..59506edea 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -198,7 +198,8 @@ class Compiled(object): @util.deprecated("0.7", ":class:`.Compiled` objects now compile " "within the constructor.") def compile(self): - """Produce the internal string representation of this element.""" + """Produce the internal string representation of this element. + """ pass def _execute_on_connection(self, connection, multiparams, params): -- cgit v1.2.1 From 741da873841012d893ec08bd77a5ecc9237eaab8 Mon Sep 17 00:00:00 2001 From: Mario Lassnig Date: Thu, 14 Nov 2013 20:18:52 +0100 Subject: added ORM support --- lib/sqlalchemy/sql/compiler.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 51ec0d9eb..4f3dbba36 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1571,8 +1571,6 @@ class SQLCompiler(Compiled): def for_update_clause(self, select): if select.for_update: - if select.for_update_of is not None: - return " FOR UPDATE OF " + select.for_update_of return " FOR UPDATE" else: return "" -- cgit v1.2.1 From e9aaf8eb66343f247b1ec2189707f820e20a0629 Mon Sep 17 00:00:00 2001 From: Mario Lassnig Date: Thu, 28 Nov 2013 14:50:41 +0100 Subject: added LockmodeArgs --- lib/sqlalchemy/sql/compiler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 4f3dbba36..54eb1f9eb 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1570,7 +1570,12 @@ class SQLCompiler(Compiled): return "" def for_update_clause(self, select): - if select.for_update: + # backwards compatibility + if isinstance(select.for_update, bool): + return " FOR UPDATE" if select.for_update else "" + elif isinstance(select.for_update, str): + return " FOR UPDATE" + elif select.for_update.mode is not None: return " FOR UPDATE" else: return "" -- cgit v1.2.1 From bb60a8ad946dd331f546f06a156b7ebb87d1709d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 28 Nov 2013 12:37:15 -0500 Subject: - work in progress, will squash --- lib/sqlalchemy/sql/compiler.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 7725196ff..0fc99897e 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1515,7 +1515,7 @@ class SQLCompiler(Compiled): order_by_select=order_by_select, **kwargs) if select._limit is not None or select._offset is not None: text += self.limit_clause(select) - if select.for_update: + if select._for_update_arg: text += self.for_update_clause(select) if self.ctes and \ @@ -1571,15 +1571,7 @@ class SQLCompiler(Compiled): return "" def for_update_clause(self, select): - # backwards compatibility - if isinstance(select.for_update, bool): - return " FOR UPDATE" if select.for_update else "" - elif isinstance(select.for_update, str): - return " FOR UPDATE" - elif select.for_update.mode is not None: - return " FOR UPDATE" - else: - return "" + return " FOR UPDATE" def returning_clause(self, stmt, returning_cols): raise exc.CompileError( -- cgit v1.2.1 From 4aaf3753d75c68050c136e734c29aae5ff9504b4 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 28 Nov 2013 22:25:09 -0500 Subject: - fix up rendering of "of" - move out tests, dialect specific out of compiler, compiler tests use new API, legacy API tests in test_selecatble - add support for adaptation of ForUpdateArg, alias support in compilers --- lib/sqlalchemy/sql/compiler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 0fc99897e..3ba3957d6 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1513,9 +1513,11 @@ class SQLCompiler(Compiled): text += self.order_by_clause(select, order_by_select=order_by_select, **kwargs) + if select._limit is not None or select._offset is not None: text += self.limit_clause(select) - if select._for_update_arg: + + if select._for_update_arg is not None: text += self.for_update_clause(select) if self.ctes and \ -- cgit v1.2.1 From 6c83ef761beb162981615fba1c22dc1c0f380568 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 29 Nov 2013 14:36:24 -0500 Subject: - New improvements to the :func:`.text` construct, including more flexible ways to set up bound parameters and return types; in particular, a :func:`.text` can now be turned into a full FROM-object, embeddable in other statements as an alias or CTE using the new method :meth:`.TextClause.columns`. [ticket:2877] --- lib/sqlalchemy/sql/compiler.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 3ba3957d6..0c252089c 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -586,17 +586,10 @@ class SQLCompiler(Compiled): return text def visit_textclause(self, textclause, **kwargs): - if textclause.typemap is not None: - for colname, type_ in textclause.typemap.items(): - self.result_map[colname - if self.dialect.case_sensitive - else colname.lower()] = \ - (colname, None, type_) - def do_bindparam(m): name = m.group(1) - if name in textclause.bindparams: - return self.process(textclause.bindparams[name]) + if name in textclause._bindparams: + return self.process(textclause._bindparams[name]) else: return self.bindparam_string(name, **kwargs) @@ -606,6 +599,33 @@ class SQLCompiler(Compiled): self.post_process_text(textclause.text)) ) + def visit_text_as_from(self, taf, iswrapper=False, + compound_index=0, force_result_map=False, + asfrom=False, + parens=True, **kw): + + toplevel = not self.stack + entry = self._default_stack_entry if toplevel else self.stack[-1] + + populate_result_map = force_result_map or ( + compound_index == 0 and ( + toplevel or \ + entry['iswrapper'] + ) + ) + + if populate_result_map: + for c in taf.c: + self._add_to_result_map( + c.key, c.key, (c,), c.type + ) + + text = self.process(taf.element, **kw) + if asfrom and parens: + text = "(%s)" % text + return text + + def visit_null(self, expr, **kw): return 'NULL' @@ -726,6 +746,7 @@ class SQLCompiler(Compiled): def function_argspec(self, func, **kwargs): return func.clause_expr._compiler_dispatch(self, **kwargs) + def visit_compound_select(self, cs, asfrom=False, parens=True, compound_index=0, **kwargs): toplevel = not self.stack -- cgit v1.2.1 From d5384d601107b76bca1634c55fb72372b2d68d42 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Wed, 11 Dec 2013 20:00:39 -0500 Subject: - implement "literal binds" for the text() clause, [ticket:2882] --- lib/sqlalchemy/sql/compiler.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 0c252089c..3c8d71331 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -585,13 +585,13 @@ class SQLCompiler(Compiled): def post_process_text(self, text): return text - def visit_textclause(self, textclause, **kwargs): + def visit_textclause(self, textclause, **kw): def do_bindparam(m): name = m.group(1) if name in textclause._bindparams: - return self.process(textclause._bindparams[name]) + return self.process(textclause._bindparams[name], **kw) else: - return self.bindparam_string(name, **kwargs) + return self.bindparam_string(name, **kw) # un-escape any \:params return BIND_PARAMS_ESC.sub(lambda m: m.group(1), -- cgit v1.2.1 From 207aaf2f41cff5970b34999d3cfc845a3b0df29c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 16 Dec 2013 19:32:10 -0500 Subject: - for [ticket:2651], leaving CheckConstraint alone, preferring to keep backwards compatibility. A note about backslashing escapes is added. Because the Text() construct now supports bind params better, the example given in the code raises an exception now, so that should cover us. The exception itself has been enhanced to include the key name of the bound param. We're backporting this to 0.8 but 0.8 doesn't have the text->bind behavior that raises. --- lib/sqlalchemy/sql/compiler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 3c8d71331..1d38c9ad3 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -970,8 +970,9 @@ class SQLCompiler(Compiled): (within_columns_clause and \ self.ansi_bind_rules): if bindparam.value is None: - raise exc.CompileError("Bind parameter without a " - "renderable value not allowed here.") + raise exc.CompileError("Bind parameter '%s' without a " + "renderable value not allowed here." + % bindparam.key) return self.render_literal_bindparam(bindparam, within_columns_clause=True, **kwargs) -- cgit v1.2.1 From 5f76f29c15b7a23cfe29c5fbd22ad02452b6a2c0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 19 Dec 2013 16:02:14 -0500 Subject: - Fixed bug with :meth:`.Insert.from_select` method where the order of the given names would not be taken into account when generating the INSERT statement, thus producing a mismatch versus the column names in the given SELECT statement. Also noted that :meth:`.Insert.from_select` implies that Python-side insert defaults cannot be used, since the statement has no VALUES clause. [ticket:2895] --- lib/sqlalchemy/sql/compiler.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 1d38c9ad3..bd886bd40 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1965,10 +1965,17 @@ class SQLCompiler(Compiled): elif c.server_onupdate is not None: self.postfetch.append(c) - # iterating through columns at the top to maintain ordering. - # otherwise we might iterate through individual sets of - # "defaults", "primary key cols", etc. - for c in stmt.table.columns: + if self.isinsert and stmt.select_names: + # for an insert from select, we can only use names that + # are given, so only select for those names. + cols = (stmt.table.c[elements._column_as_key(name)] + for name in stmt.select_names) + else: + # iterate through all table columns to maintain + # ordering, even for those cols that aren't included + cols = stmt.table.columns + + for c in cols: if c.key in parameters and c.key not in check_columns: value = parameters.pop(c.key) if elements._is_literal(value): -- cgit v1.2.1 From 65bd6ec96602ab8b755b9bc25638957a09477de6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 20 Dec 2013 10:26:09 -0500 Subject: - Fixed issue where a primary key column that has a Sequence on it, yet the column is not the "auto increment" column, either because it has a foreign key constraint or ``autoincrement=False`` set, would attempt to fire the Sequence on INSERT for backends that don't support sequences, when presented with an INSERT missing the primary key value. This would take place on non-sequence backends like SQLite, MySQL. [ticket:2896] --- lib/sqlalchemy/sql/compiler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index bd886bd40..b09900570 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2035,7 +2035,13 @@ class SQLCompiler(Compiled): else: self.returning.append(c) else: - if c.default is not None or \ + if ( + c.default is not None and + ( + not c.default.is_sequence or + self.dialect.supports_sequences + ) + ) or \ c is stmt.table._autoincrement_column and ( self.dialect.supports_sequences or self.dialect.preexecute_autoincrement_sequences -- cgit v1.2.1 From f89d4d216bd7605c920b7b8a10ecde6bfea2238c Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 5 Jan 2014 16:57:05 -0500 Subject: - happy new year --- lib/sqlalchemy/sql/compiler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index b09900570..ade3c623a 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1,5 +1,5 @@ # sql/compiler.py -# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors +# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors # # This module is part of SQLAlchemy and is released under # the MIT License: http://www.opensource.org/licenses/mit-license.php -- cgit v1.2.1 From cf8e5e3cf5b0e1be05a611c8828690acfcd2b9fa Mon Sep 17 00:00:00 2001 From: donkopotamus Date: Fri, 17 Jan 2014 11:00:24 +1300 Subject: Bug Fix: Stop generating bad sql if an empty UniqueConstraint() is given --- lib/sqlalchemy/sql/compiler.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index ade3c623a..5c5bfad55 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2511,6 +2511,8 @@ class DDLCompiler(Compiled): return preparer.format_table(table) def visit_unique_constraint(self, constraint): + if len(constraint) == 0: + return '' text = "" if constraint.name is not None: text += "CONSTRAINT %s " % \ -- cgit v1.2.1 From b9318c98637bbd5c19267728fcfe941668345325 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 20 Jan 2014 21:01:35 -0500 Subject: - Fixed the multiple-table "UPDATE..FROM" construct, only usable on MySQL, to correctly render the SET clause among multiple columns with the same name across tables. This also changes the name used for the bound parameter in the SET clause to "_" for the non-primary table only; as this parameter is typically specified using the :class:`.Column` object directly this should not have an impact on applications. The fix takes effect for both :meth:`.Table.update` as well as :meth:`.Query.update` in the ORM. [ticket:2912] --- lib/sqlalchemy/sql/compiler.py | 110 ++++++++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 30 deletions(-) (limited to 'lib/sqlalchemy/sql/compiler.py') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 5c5bfad55..4448f7c7b 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -28,6 +28,7 @@ from . import schema, sqltypes, operators, functions, \ from .. import util, exc import decimal import itertools +import operator RESERVED_WORDS = set([ 'all', 'analyse', 'analyze', 'and', 'any', 'array', @@ -1771,7 +1772,7 @@ class SQLCompiler(Compiled): table_text = self.update_tables_clause(update_stmt, update_stmt.table, extra_froms, **kw) - colparams = self._get_colparams(update_stmt, extra_froms, **kw) + colparams = self._get_colparams(update_stmt, **kw) if update_stmt._hints: dialect_hints = dict([ @@ -1840,7 +1841,40 @@ class SQLCompiler(Compiled): bindparam._is_crud = True return bindparam._compiler_dispatch(self) - def _get_colparams(self, stmt, extra_tables=None, **kw): + @util.memoized_property + def _key_getters_for_crud_column(self): + if self.isupdate and self.statement._extra_froms: + # when extra tables are present, refer to the columns + # in those extra tables as table-qualified, including in + # dictionaries and when rendering bind param names. + # the "main" table of the statement remains unqualified, + # allowing the most compatibility with a non-multi-table + # statement. + _et = set(self.statement._extra_froms) + def _column_as_key(key): + str_key = elements._column_as_key(key) + if hasattr(key, 'table') and key.table in _et: + return (key.table.name, str_key) + else: + return str_key + def _getattr_col_key(col): + if col.table in _et: + return (col.table.name, col.key) + else: + return col.key + def _col_bind_name(col): + if col.table in _et: + return "%s_%s" % (col.table.name, col.key) + else: + return col.key + + else: + _column_as_key = elements._column_as_key + _getattr_col_key = _col_bind_name = operator.attrgetter("key") + + return _column_as_key, _getattr_col_key, _col_bind_name + + def _get_colparams(self, stmt, **kw): """create a set of tuples representing column/string pairs for use in an INSERT or UPDATE statement. @@ -1869,12 +1903,18 @@ class SQLCompiler(Compiled): else: stmt_parameters = stmt.parameters + # getters - these are normally just column.key, + # but in the case of mysql multi-table update, the rules for + # .key must conditionally take tablename into account + _column_as_key, _getattr_col_key, _col_bind_name = \ + self._key_getters_for_crud_column + # if we have statement parameters - set defaults in the # compiled params if self.column_keys is None: parameters = {} else: - parameters = dict((elements._column_as_key(key), REQUIRED) + parameters = dict((_column_as_key(key), REQUIRED) for key in self.column_keys if not stmt_parameters or key not in stmt_parameters) @@ -1884,7 +1924,7 @@ class SQLCompiler(Compiled): if stmt_parameters is not None: for k, v in stmt_parameters.items(): - colkey = elements._column_as_key(k) + colkey = _column_as_key(k) if colkey is not None: parameters.setdefault(colkey, v) else: @@ -1892,7 +1932,9 @@ class SQLCompiler(Compiled): # add it to values() in an "as-is" state, # coercing right side to bound param if elements._is_literal(v): - v = self.process(elements.BindParameter(None, v, type_=k.type), **kw) + v = self.process( + elements.BindParameter(None, v, type_=k.type), + **kw) else: v = self.process(v.self_group(), **kw) @@ -1922,24 +1964,25 @@ class SQLCompiler(Compiled): postfetch_lastrowid = need_pks and self.dialect.postfetch_lastrowid check_columns = {} + # special logic that only occurs for multi-table UPDATE # statements - if extra_tables and stmt_parameters: + if self.isupdate and stmt._extra_froms and stmt_parameters: normalized_params = dict( (elements._clause_element_as_expr(c), param) for c, param in stmt_parameters.items() ) - assert self.isupdate affected_tables = set() - for t in extra_tables: + for t in stmt._extra_froms: for c in t.c: if c in normalized_params: affected_tables.add(t) - check_columns[c.key] = c + check_columns[_getattr_col_key(c)] = c value = normalized_params[c] if elements._is_literal(value): value = self._create_crud_bind_param( - c, value, required=value is REQUIRED) + c, value, required=value is REQUIRED, + name=_col_bind_name(c)) else: self.postfetch.append(c) value = self.process(value.self_group(), **kw) @@ -1954,12 +1997,18 @@ class SQLCompiler(Compiled): elif c.onupdate is not None and not c.onupdate.is_sequence: if c.onupdate.is_clause_element: values.append( - (c, self.process(c.onupdate.arg.self_group(), **kw)) + (c, self.process( + c.onupdate.arg.self_group(), + **kw) + ) ) self.postfetch.append(c) else: values.append( - (c, self._create_crud_bind_param(c, None)) + (c, self._create_crud_bind_param( + c, None, name=_col_bind_name(c) + ) + ) ) self.prefetch.append(c) elif c.server_onupdate is not None: @@ -1968,7 +2017,7 @@ class SQLCompiler(Compiled): if self.isinsert and stmt.select_names: # for an insert from select, we can only use names that # are given, so only select for those names. - cols = (stmt.table.c[elements._column_as_key(name)] + cols = (stmt.table.c[_column_as_key(name)] for name in stmt.select_names) else: # iterate through all table columns to maintain @@ -1976,14 +2025,15 @@ class SQLCompiler(Compiled): cols = stmt.table.columns for c in cols: - if c.key in parameters and c.key not in check_columns: - value = parameters.pop(c.key) + col_key = _getattr_col_key(c) + if col_key in parameters and col_key not in check_columns: + value = parameters.pop(col_key) if elements._is_literal(value): value = self._create_crud_bind_param( c, value, required=value is REQUIRED, - name=c.key + name=_col_bind_name(c) if not stmt._has_multi_parameters - else "%s_0" % c.key + else "%s_0" % _col_bind_name(c) ) else: if isinstance(value, elements.BindParameter) and \ @@ -2119,12 +2169,12 @@ class SQLCompiler(Compiled): if parameters and stmt_parameters: check = set(parameters).intersection( - elements._column_as_key(k) for k in stmt.parameters + _column_as_key(k) for k in stmt.parameters ).difference(check_columns) if check: raise exc.CompileError( "Unconsumed column names: %s" % - (", ".join(check)) + (", ".join("%s" % c for c in check)) ) if stmt._has_multi_parameters: @@ -2133,17 +2183,17 @@ class SQLCompiler(Compiled): values.extend( [ - ( - c, - self._create_crud_bind_param( - c, row[c.key], - name="%s_%d" % (c.key, i + 1) - ) - if c.key in row else param - ) - for (c, param) in values_0 - ] - for i, row in enumerate(stmt.parameters[1:]) + ( + c, + self._create_crud_bind_param( + c, row[c.key], + name="%s_%d" % (c.key, i + 1) + ) + if c.key in row else param + ) + for (c, param) in values_0 + ] + for i, row in enumerate(stmt.parameters[1:]) ) return values -- cgit v1.2.1