diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-08-15 15:08:09 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2020-08-17 11:29:51 -0400 |
commit | 3b4bbbb2a3d337d0af1ba5ccb0d29d1c48735e83 (patch) | |
tree | b3d3e90e7d0f22451e8d4e9e29ef82664e6dd4d4 /lib/sqlalchemy/sql/compiler.py | |
parent | 8a274e0058183cebeb37d3a5e7903209ce5e7c0e (diff) | |
download | sqlalchemy-3b4bbbb2a3d337d0af1ba5ccb0d29d1c48735e83.tar.gz |
Create a real type for Tuple() and handle appropriately in compiler
Improved the :func:`_sql.tuple_` construct such that it behaves predictably
when used in a columns-clause context. The SQL tuple is not supported as a
"SELECT" columns clause element on most backends; on those that do
(PostgreSQL, not surprisingly), the Python DBAPI does not have a "nested
type" concept so there are still challenges in fetching rows for such an
object. Use of :func:`_sql.tuple_` in a :func:`_sql.select` or
:class:`_orm.Query` will now raise a :class:`_exc.CompileError` at the
point at which the :func:`_sql.tuple_` object is seen as presenting itself
for fetching rows (i.e., if the tuple is in the columns clause of a
subquery, no error is raised). For ORM use,the :class:`_orm.Bundle` object
is an explicit directive that a series of columns should be returned as a
sub-tuple per row and is suggested by the error message. Additionally ,the
tuple will now render with parenthesis in all contexts. Previously, the
parenthesization would not render in a columns context leading to
non-defined behavior.
As part of this change, Tuple receives a dedicated datatype
which appears to allow us the very desirable change of removing
the bindparam._expanding_in_types attribute as well as
ClauseList._tuple_values (which might already have not been
needed due to #4645).
Fixes: #5127
Change-Id: Iecafa0e0aac2f1f37ec8d0e1631d562611c90200
Diffstat (limited to 'lib/sqlalchemy/sql/compiler.py')
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 53 |
1 files changed, 34 insertions, 19 deletions
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 542bf58ac..4f4cf7f8b 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -848,10 +848,10 @@ class SQLCompiler(Compiled): ( self.bind_names[bindparam], bindparam.type._cached_bind_processor(self.dialect) - if not bindparam._expanding_in_types + if not bindparam.type._is_tuple_type else tuple( elem_type._cached_bind_processor(self.dialect) - for elem_type in bindparam._expanding_in_types + for elem_type in bindparam.type.types ), ) for bindparam in self.bind_names @@ -1018,9 +1018,9 @@ class SQLCompiler(Compiled): if bindparam in literal_execute_params: continue - if bindparam._expanding_in_types: + if bindparam.type._is_tuple_type: inputsizes[bindparam] = [ - _lookup_type(typ) for typ in bindparam._expanding_in_types + _lookup_type(typ) for typ in bindparam.type.types ] else: inputsizes[bindparam] = _lookup_type(bindparam.type) @@ -1107,7 +1107,7 @@ class SQLCompiler(Compiled): if not parameter.literal_execute: parameters.update(to_update) - if parameter._expanding_in_types: + if parameter.type._is_tuple_type: new_processors.update( ( "%s_%s_%s" % (name, i, j), @@ -1541,6 +1541,9 @@ class SQLCompiler(Compiled): if s ) + def visit_tuple(self, clauselist, **kw): + return "(%s)" % self.visit_clauselist(clauselist, **kw) + def visit_clauselist(self, clauselist, **kw): sep = clauselist.operator if sep is None: @@ -1548,10 +1551,7 @@ class SQLCompiler(Compiled): else: sep = OPERATORS[clauselist.operator] - text = self._generate_delimited_list(clauselist.clauses, sep, **kw) - if clauselist._tuple_values and self.dialect.tuple_in_values: - text = "VALUES " + text - return text + return self._generate_delimited_list(clauselist.clauses, sep, **kw) def visit_case(self, clause, **kwargs): x = "CASE " @@ -1824,27 +1824,31 @@ class SQLCompiler(Compiled): def _literal_execute_expanding_parameter_literal_binds( self, parameter, values ): + if not values: + assert not parameter.type._is_tuple_type replacement_expression = self.visit_empty_set_expr( - parameter._expanding_in_types - if parameter._expanding_in_types - else [parameter.type] + [parameter.type] ) elif isinstance(values[0], (tuple, list)): + assert parameter.type._is_tuple_type replacement_expression = ( "VALUES " if self.dialect.tuple_in_values else "" ) + ", ".join( "(%s)" % ( ", ".join( - self.render_literal_value(value, parameter.type) - for value in tuple_element + self.render_literal_value(value, param_type) + for value, param_type in zip( + tuple_element, parameter.type.types + ) ) ) for i, tuple_element in enumerate(values) ) else: + assert not parameter.type._is_tuple_type replacement_expression = ", ".join( self.render_literal_value(value, parameter.type) for value in values @@ -1853,6 +1857,7 @@ class SQLCompiler(Compiled): return (), replacement_expression def _literal_execute_expanding_parameter(self, name, parameter, values): + if parameter.literal_execute: return self._literal_execute_expanding_parameter_literal_binds( parameter, values @@ -1860,11 +1865,15 @@ class SQLCompiler(Compiled): if not values: to_update = [] - replacement_expression = self.visit_empty_set_expr( - parameter._expanding_in_types - if parameter._expanding_in_types - else [parameter.type] - ) + if parameter.type._is_tuple_type: + + replacement_expression = self.visit_empty_set_expr( + parameter.type.types + ) + else: + replacement_expression = self.visit_empty_set_expr( + [parameter.type] + ) elif isinstance(values[0], (tuple, list)): to_update = [ @@ -2560,6 +2569,12 @@ class SQLCompiler(Compiled): if keyname is None: self._ordered_columns = False self._textual_ordered_columns = True + if type_._is_tuple_type: + raise exc.CompileError( + "Most backends don't support SELECTing " + "from a tuple() object. If this is an ORM query, " + "consider using the Bundle object." + ) self._result_columns.append((keyname, name, objects, type_)) def _label_select_column( |