From fc73036865a0c0d2809b66fcfdc663ff425f4267 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Thu, 24 Sep 2015 15:07:31 +0200 Subject: Preserve order in update method In some DBs the UPDATE operation is order dependent, so the operation behaves differently depending on the order of the values. As an example, imagine a volumes table with columns 'status' and 'previous_status' and we want to update a volume that has 'available' in the status column. If the SQL query is performed as: UPDATE volumes SET previous_status=status, status='new' WHERE id=1; This will result in a volume with 'new' status and 'available' previous_status both on SQLite and MariaDB, but if we reverse the columns: UPDATE volumes SET status='new', previous_status=status WHERE id=1; We will get the same result in SQLite but will result in a volume with status and previous_status set to 'new' in MariaDB, which is not what we want. So order must be taken into consideration in some cases and it should be allowed to ve specified via the Query update method or the values method of an update. This patch fixes this issue by preserving the order of parameters in updates and allowing to receive not only dictionaries in update and values but also ordered dictionaries and list/tuples of value pairs (like dict and OrderedDict do). fixes #3541 --- lib/sqlalchemy/sql/compiler.py | 3 ++- lib/sqlalchemy/sql/crud.py | 26 ++++++++++++++++++-------- lib/sqlalchemy/sql/dml.py | 15 +++++++++++++-- 3 files changed, 33 insertions(+), 11 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 691195772..768d4f83a 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1971,7 +1971,8 @@ class SQLCompiler(Compiled): table_text = self.update_tables_clause(update_stmt, update_stmt.table, extra_froms, **kw) - crud_params = crud._get_crud_params(self, update_stmt, **kw) + crud_params = crud._get_crud_params(self, update_stmt, keep_order=True, + **kw) if update_stmt._hints: dialect_hints, table_text = self._setup_crud_hints( diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index e6f16b698..614f9413b 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -26,7 +26,7 @@ values present. """) -def _get_crud_params(compiler, stmt, **kw): +def _get_crud_params(compiler, stmt, keep_order=False, **kw): """create a set of tuples representing column/string pairs for use in an INSERT or UPDATE statement. @@ -64,12 +64,12 @@ def _get_crud_params(compiler, stmt, **kw): # if we have statement parameters - set defaults in the # compiled params if compiler.column_keys is None: - parameters = {} + parameters = util.OrderedDict() else: - parameters = dict((_column_as_key(key), REQUIRED) - for key in compiler.column_keys - if not stmt_parameters or - key not in stmt_parameters) + parameters = util.OrderedDict((_column_as_key(key), REQUIRED) + for key in compiler.column_keys + if not stmt_parameters or + key not in stmt_parameters) # create a list of column assignment clauses as tuples values = [] @@ -97,7 +97,7 @@ def _get_crud_params(compiler, stmt, **kw): _scan_cols( compiler, stmt, parameters, _getattr_col_key, _column_as_key, - _col_bind_name, check_columns, values, kw) + _col_bind_name, check_columns, values, kw, keep_order=keep_order) if parameters and stmt_parameters: check = set(parameters).intersection( @@ -202,7 +202,7 @@ def _scan_insert_from_select_cols( def _scan_cols( compiler, stmt, parameters, _getattr_col_key, - _column_as_key, _col_bind_name, check_columns, values, kw): + _column_as_key, _col_bind_name, check_columns, values, kw, keep_order): need_pks, implicit_returning, \ implicit_return_defaults, postfetch_lastrowid = \ @@ -210,6 +210,16 @@ def _scan_cols( cols = stmt.table.columns + if keep_order: + # Order columns with parameters first, preserving their original order, + # and then the rest of the columns + keys = tuple(parameters.keys()) if parameters else tuple() + table_cols = tuple(cols) + cols = sorted(table_cols, + key=(lambda x: keys.index(_getattr_col_key(x)) + if _getattr_col_key(x) in keys + else len(keys) + table_cols.index(x))) + for c in cols: col_key = _getattr_col_key(c) if col_key in parameters and col_key not in check_columns: diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 6756f1554..983fed2b5 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -15,6 +15,7 @@ from .elements import ClauseElement, _literal_as_text, Null, and_, _clone, \ from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes from .. import util from .. import exc +from sqlalchemy.sql import schema class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): @@ -30,16 +31,26 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): _prefixes = () def _process_colparams(self, parameters): + def is_value_pair_dict(params): + # Check if params is a value list/tuple representing a dictionary + return ( + isinstance(params, (list, tuple)) and + all(isinstance(p, (list, tuple)) and len(p) == 2 and + isinstance(p[0], schema.Column) for p in params)) + def process_single(p): if isinstance(p, (list, tuple)): - return dict( + if is_value_pair_dict(p): + return util.OrderedDict(p) + return util.OrderedDict( (c.key, pval) for c, pval in zip(self.table.c, p) ) else: return p - if (isinstance(parameters, (list, tuple)) and parameters and + if (not is_value_pair_dict(parameters) and + isinstance(parameters, (list, tuple)) and parameters and isinstance(parameters[0], (list, tuple, dict))): if not self._supports_multi_parameters: -- cgit v1.2.1 From 9fb76d759678558f6fd087dcc04de3f2daa3a571 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Tue, 29 Sep 2015 20:29:33 +0200 Subject: Only preserve order in updates if tuple/dict To avoid penalties for updates that do not require ordering, we will only use OrderedDict for updates that receive a tuple or list of pairs, and all kinds of dictionaries (dict, sqlalchemy's OrderedDict, or collections.OrderedDict) will be treateated as unordered updates, just like we were doing before. This way this new feature will not change how updates behave for any existing code and will only affect those that use the new ordered feature. This patch reverts update tests to how they were before as well as adds a couple of tests to confirm that OrderedDicts are really treated like normal dicts. --- lib/sqlalchemy/sql/compiler.py | 3 +-- lib/sqlalchemy/sql/crud.py | 21 +++++++++++++++------ lib/sqlalchemy/sql/dml.py | 15 ++++----------- lib/sqlalchemy/sql/util.py | 7 +++++++ 4 files changed, 27 insertions(+), 19 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 768d4f83a..691195772 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1971,8 +1971,7 @@ class SQLCompiler(Compiled): table_text = self.update_tables_clause(update_stmt, update_stmt.table, extra_froms, **kw) - crud_params = crud._get_crud_params(self, update_stmt, keep_order=True, - **kw) + crud_params = crud._get_crud_params(self, update_stmt, **kw) if update_stmt._hints: dialect_hints, table_text = self._setup_crud_hints( diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index 614f9413b..235889ad9 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -14,6 +14,8 @@ from .. import exc from . import elements import operator +from sqlalchemy.sql import util as sql_util + REQUIRED = util.symbol('REQUIRED', """ Placeholder for the value within a :class:`.BindParameter` which is required to be present when the statement is passed @@ -26,7 +28,7 @@ values present. """) -def _get_crud_params(compiler, stmt, keep_order=False, **kw): +def _get_crud_params(compiler, stmt, **kw): """create a set of tuples representing column/string pairs for use in an INSERT or UPDATE statement. @@ -61,15 +63,22 @@ def _get_crud_params(compiler, stmt, keep_order=False, **kw): _column_as_key, _getattr_col_key, _col_bind_name = \ _key_getters_for_crud_column(compiler) + # We have to keep parameters' order if we are doing an update and the + # statement paramenters are a list or tuple of pairs. It would also work + # without isupdate check, but adding it shortcircuits the boolean operation + # resulting in false for all inserts. + keep_order = (compiler.isupdate + and sql_util.is_value_pair_dict(stmt.parameters)) + dict_type = util.OrderedDict if keep_order else dict # if we have statement parameters - set defaults in the # compiled params if compiler.column_keys is None: - parameters = util.OrderedDict() + parameters = dict_type() else: - parameters = util.OrderedDict((_column_as_key(key), REQUIRED) - for key in compiler.column_keys - if not stmt_parameters or - key not in stmt_parameters) + parameters = dict_type((_column_as_key(key), REQUIRED) + for key in compiler.column_keys + if not stmt_parameters or + key not in stmt_parameters) # create a list of column assignment clauses as tuples values = [] diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 983fed2b5..c8407e3fd 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -15,7 +15,7 @@ from .elements import ClauseElement, _literal_as_text, Null, and_, _clone, \ from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes from .. import util from .. import exc -from sqlalchemy.sql import schema +from sqlalchemy.sql import util as sql_util class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): @@ -31,25 +31,18 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): _prefixes = () def _process_colparams(self, parameters): - def is_value_pair_dict(params): - # Check if params is a value list/tuple representing a dictionary - return ( - isinstance(params, (list, tuple)) and - all(isinstance(p, (list, tuple)) and len(p) == 2 and - isinstance(p[0], schema.Column) for p in params)) - def process_single(p): if isinstance(p, (list, tuple)): - if is_value_pair_dict(p): + if sql_util.is_value_pair_dict(p): return util.OrderedDict(p) - return util.OrderedDict( + return dict( (c.key, pval) for c, pval in zip(self.table.c, p) ) else: return p - if (not is_value_pair_dict(parameters) and + if (not sql_util.is_value_pair_dict(parameters) and isinstance(parameters, (list, tuple)) and parameters and isinstance(parameters[0], (list, tuple, dict))): diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index cbd74faac..c73f710af 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -29,6 +29,7 @@ join_condition = util.langhelpers.public_factory( from .annotation import _shallow_annotate, _deep_annotate, _deep_deannotate from .elements import _find_columns from .ddl import sort_tables +from sqlalchemy.sql import schema def find_join_source(clauses, join_to): @@ -436,6 +437,12 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, return pairs +def is_value_pair_dict(params): + """Check if params is a value list/tuple representing a dictionary.""" + return (isinstance(params, (list, tuple)) and + all(isinstance(p, (list, tuple)) and len(p) == 2 and + isinstance(p[0], schema.Column) for p in params)) + class ClauseAdapter(visitors.ReplacingCloningVisitor): """Clones and modifies clauses based on column correspondence. -- cgit v1.2.1 From ddb43cebf68b9e6194a1a22b7630e143d2a79c00 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Wed, 30 Sep 2015 18:45:05 +0200 Subject: Postpone parameters change in ordered updates Postpone as much as possible the change of update parameters to OrderedDict from list or tuple of pairs. This way we won't have problems with query's update method. --- lib/sqlalchemy/sql/crud.py | 9 +++++++-- lib/sqlalchemy/sql/dml.py | 10 +++------- lib/sqlalchemy/sql/util.py | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index 235889ad9..0ab52a13e 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -68,8 +68,13 @@ def _get_crud_params(compiler, stmt, **kw): # without isupdate check, but adding it shortcircuits the boolean operation # resulting in false for all inserts. keep_order = (compiler.isupdate - and sql_util.is_value_pair_dict(stmt.parameters)) - dict_type = util.OrderedDict if keep_order else dict + and sql_util.is_value_pair_dict(stmt_parameters)) + if keep_order: + stmt_parameters = util.OrderedDict(stmt_parameters) + dict_type = util.OrderedDict + else: + dict_type = dict + # if we have statement parameters - set defaults in the # compiled params if compiler.column_keys is None: diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index c8407e3fd..dead61d9a 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -32,13 +32,9 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): def _process_colparams(self, parameters): def process_single(p): - if isinstance(p, (list, tuple)): - if sql_util.is_value_pair_dict(p): - return util.OrderedDict(p) - return dict( - (c.key, pval) - for c, pval in zip(self.table.c, p) - ) + if (isinstance(p, (list, tuple)) and + not sql_util.is_value_pair_dict(p)): + return {c.key: pval for c, pval in zip(self.table.c, p)} else: return p diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index c73f710af..dedbdfe32 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -439,7 +439,7 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, def is_value_pair_dict(params): """Check if params is a value list/tuple representing a dictionary.""" - return (isinstance(params, (list, tuple)) and + return (isinstance(params, (list, tuple)) and len(params) > 0 and all(isinstance(p, (list, tuple)) and len(p) == 2 and isinstance(p[0], schema.Column) for p in params)) -- cgit v1.2.1 From ef816f23290ae3b7f386832dd01e59dfb4eca7f7 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Fri, 2 Oct 2015 18:22:33 +0200 Subject: Only check once if parameters are ordered Instead of checking multiple times if parameters are a dictionary in the form of a tuple or list of value pairs, we check it only once and then store it in the statement so it can be used on compilation time. --- lib/sqlalchemy/sql/crud.py | 8 +++---- lib/sqlalchemy/sql/dml.py | 54 +++++++++++++++++++++++++++------------------- lib/sqlalchemy/sql/util.py | 8 ------- 3 files changed, 35 insertions(+), 35 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py index 0ab52a13e..a0ddb66cb 100644 --- a/lib/sqlalchemy/sql/crud.py +++ b/lib/sqlalchemy/sql/crud.py @@ -14,7 +14,6 @@ from .. import exc from . import elements import operator -from sqlalchemy.sql import util as sql_util REQUIRED = util.symbol('REQUIRED', """ Placeholder for the value within a :class:`.BindParameter` @@ -67,9 +66,7 @@ def _get_crud_params(compiler, stmt, **kw): # statement paramenters are a list or tuple of pairs. It would also work # without isupdate check, but adding it shortcircuits the boolean operation # resulting in false for all inserts. - keep_order = (compiler.isupdate - and sql_util.is_value_pair_dict(stmt_parameters)) - if keep_order: + if stmt._preserve_parameter_order: stmt_parameters = util.OrderedDict(stmt_parameters) dict_type = util.OrderedDict else: @@ -111,7 +108,8 @@ def _get_crud_params(compiler, stmt, **kw): _scan_cols( compiler, stmt, parameters, _getattr_col_key, _column_as_key, - _col_bind_name, check_columns, values, kw, keep_order=keep_order) + _col_bind_name, check_columns, values, kw, + keep_order=stmt._preserve_parameter_order) if parameters and stmt_parameters: check = set(parameters).intersection( diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index dead61d9a..8ba7cfa44 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -15,7 +15,7 @@ from .elements import ClauseElement, _literal_as_text, Null, and_, _clone, \ from .selectable import _interpret_as_from, _interpret_as_select, HasPrefixes from .. import util from .. import exc -from sqlalchemy.sql import util as sql_util +from sqlalchemy.sql import schema class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): @@ -31,25 +31,28 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): _prefixes = () def _process_colparams(self, parameters): + # Check if params is a value list/tuple representing a dictionary. + is_ordered = (isinstance(parameters, (list, tuple)) and + len(parameters) > 0 and + all(isinstance(p, (list, tuple)) and len(p) == 2 and + isinstance(p[0], schema.Column) for p in parameters)) + def process_single(p): - if (isinstance(p, (list, tuple)) and - not sql_util.is_value_pair_dict(p)): + if not is_ordered and isinstance(p, (list, tuple)): return {c.key: pval for c, pval in zip(self.table.c, p)} else: return p - if (not sql_util.is_value_pair_dict(parameters) and - isinstance(parameters, (list, tuple)) and parameters and - isinstance(parameters[0], (list, tuple, dict))): - + if (not is_ordered and isinstance(parameters, (list, tuple)) and + parameters and isinstance(parameters[0], (list, tuple, dict))): if not self._supports_multi_parameters: raise exc.InvalidRequestError( "This construct does not support " "multiple parameter sets.") - return [process_single(p) for p in parameters], True + return [process_single(p) for p in parameters], True, is_ordered else: - return process_single(parameters), False + return process_single(parameters), False, is_ordered def params(self, *arg, **kw): """Set the parameters for the statement. @@ -178,12 +181,16 @@ class ValuesBase(UpdateBase): _supports_multi_parameters = False _has_multi_parameters = False + _preserve_parameter_order = False select = None def __init__(self, table, values, prefixes): self.table = _interpret_as_from(table) - self.parameters, self._has_multi_parameters = \ - self._process_colparams(values) + + colparams = self._process_colparams(values) + self.parameters = colparams[0] + self._has_multi_parameters = colparams[1] + self._preserve_parameter_order = colparams[2] if prefixes: self._setup_prefixes(prefixes) @@ -314,27 +321,28 @@ class ValuesBase(UpdateBase): else: v = {} + colparams = self._process_colparams(v) + had_multi_parameters = self._has_multi_parameters + self._has_multi_parameters = colparams[1] + self._preserve_parameter_order = colparams[2] + if self.parameters is None: - self.parameters, self._has_multi_parameters = \ - self._process_colparams(v) + self.parameters = colparams[0] else: - if self._has_multi_parameters: - self.parameters = list(self.parameters) - p, self._has_multi_parameters = self._process_colparams(v) + if had_multi_parameters: if not self._has_multi_parameters: raise exc.ArgumentError( "Can't mix single-values and multiple values " "formats in one statement") - self.parameters.extend(p) + self.parameters.extend(colparams[0]) else: self.parameters = self.parameters.copy() - p, self._has_multi_parameters = self._process_colparams(v) if self._has_multi_parameters: raise exc.ArgumentError( "Can't mix single-values and multiple values " "formats in one statement") - self.parameters.update(p) + self.parameters.update(colparams[0]) if kwargs: if self._has_multi_parameters: @@ -548,9 +556,11 @@ class Insert(ValuesBase): raise exc.InvalidRequestError( "This construct already inserts value expressions") - self.parameters, self._has_multi_parameters = \ - self._process_colparams( - dict((_column_as_key(n), Null()) for n in names)) + colparams = self._process_colparams({_column_as_key(n): Null() + for n in names}) + self.parameters = colparams[0] + self._has_multi_parameters = colparams[1] + self._preserve_parameter_order = colparams[2] self.select_names = names self.inline = True diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index dedbdfe32..bf1bfd310 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -29,7 +29,6 @@ join_condition = util.langhelpers.public_factory( from .annotation import _shallow_annotate, _deep_annotate, _deep_deannotate from .elements import _find_columns from .ddl import sort_tables -from sqlalchemy.sql import schema def find_join_source(clauses, join_to): @@ -437,13 +436,6 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, return pairs -def is_value_pair_dict(params): - """Check if params is a value list/tuple representing a dictionary.""" - return (isinstance(params, (list, tuple)) and len(params) > 0 and - all(isinstance(p, (list, tuple)) and len(p) == 2 and - isinstance(p[0], schema.Column) for p in params)) - - class ClauseAdapter(visitors.ReplacingCloningVisitor): """Clones and modifies clauses based on column correspondence. -- cgit v1.2.1 From c6ba1a7d4c2b1c081cfff39b3455d1684ec147be Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Mon, 5 Oct 2015 10:42:09 +0200 Subject: Remove dict comprehension for py26 compatibility Remove added dict comprehensions that make this patch set non python 2.6 compatible. --- lib/sqlalchemy/sql/dml.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 8ba7cfa44..3de54327a 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -39,7 +39,7 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): def process_single(p): if not is_ordered and isinstance(p, (list, tuple)): - return {c.key: pval for c, pval in zip(self.table.c, p)} + return dict((c.key, pval) for c, pval in zip(self.table.c, p)) else: return p @@ -556,8 +556,8 @@ class Insert(ValuesBase): raise exc.InvalidRequestError( "This construct already inserts value expressions") - colparams = self._process_colparams({_column_as_key(n): Null() - for n in names}) + colparams = self._process_colparams(dict((_column_as_key(n), Null()) + for n in names)) self.parameters = colparams[0] self._has_multi_parameters = colparams[1] self._preserve_parameter_order = colparams[2] -- cgit v1.2.1 From 44e5a31ccee5335962602327132a4196fb1c7911 Mon Sep 17 00:00:00 2001 From: Gorka Eguileor Date: Thu, 8 Oct 2015 13:24:02 +0200 Subject: Reduce the numbers of calls to isinstance Change _process_colparams method to remove duplicate isinstance calls and try to speed up processing of the parameters. --- lib/sqlalchemy/sql/dml.py | 66 +++++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 34 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index 3de54327a..7243e56e1 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -31,28 +31,28 @@ class UpdateBase(DialectKWArgs, HasPrefixes, Executable, ClauseElement): _prefixes = () def _process_colparams(self, parameters): - # Check if params is a value list/tuple representing a dictionary. - is_ordered = (isinstance(parameters, (list, tuple)) and - len(parameters) > 0 and - all(isinstance(p, (list, tuple)) and len(p) == 2 and - isinstance(p[0], schema.Column) for p in parameters)) - def process_single(p): - if not is_ordered and isinstance(p, (list, tuple)): + if isinstance(p, (list, tuple)): return dict((c.key, pval) for c, pval in zip(self.table.c, p)) else: return p - if (not is_ordered and isinstance(parameters, (list, tuple)) and - parameters and isinstance(parameters[0], (list, tuple, dict))): - if not self._supports_multi_parameters: - raise exc.InvalidRequestError( - "This construct does not support " - "multiple parameter sets.") + if parameters and isinstance(parameters, (list, tuple)): + p0 = parameters[0] + is_lt = isinstance(p0, (list, tuple)) + # If it's an ordered dict in the form of value pairs return it + if is_lt and len(p0) == 2 and isinstance(p0[0], schema.Column): + return parameters, False, True - return [process_single(p) for p in parameters], True, is_ordered - else: - return process_single(parameters), False, is_ordered + if is_lt or isinstance(p0, dict): + if not self._supports_multi_parameters: + raise exc.InvalidRequestError( + "This construct does not support " + "multiple parameter sets.") + + return [process_single(p) for p in parameters], True, False + + return process_single(parameters), False, False def params(self, *arg, **kw): """Set the parameters for the statement. @@ -187,10 +187,8 @@ class ValuesBase(UpdateBase): def __init__(self, table, values, prefixes): self.table = _interpret_as_from(table) - colparams = self._process_colparams(values) - self.parameters = colparams[0] - self._has_multi_parameters = colparams[1] - self._preserve_parameter_order = colparams[2] + self.parameters, self._has_multi_parameters, \ + self._preserve_parameter_order = self._process_colparams(values) if prefixes: self._setup_prefixes(prefixes) @@ -321,28 +319,30 @@ class ValuesBase(UpdateBase): else: v = {} - colparams = self._process_colparams(v) - had_multi_parameters = self._has_multi_parameters - self._has_multi_parameters = colparams[1] - self._preserve_parameter_order = colparams[2] - if self.parameters is None: - self.parameters = colparams[0] + self.parameters, self._has_multi_parameters, \ + self._preserve_parameter_order = self._process_colparams(v) else: - if had_multi_parameters: + if self._has_multi_parameters: + self.parameters = list(self.parameters) + p, self._has_multi_parameters, \ + self._preserve_parameter_order = self._process_colparams(v) + if not self._has_multi_parameters: raise exc.ArgumentError( "Can't mix single-values and multiple values " "formats in one statement") - self.parameters.extend(colparams[0]) + self.parameters.extend(p) else: self.parameters = self.parameters.copy() + p, self._has_multi_parameters, \ + self._preserve_parameter_order = self._process_colparams(v) if self._has_multi_parameters: raise exc.ArgumentError( "Can't mix single-values and multiple values " "formats in one statement") - self.parameters.update(colparams[0]) + self.parameters.update(p) if kwargs: if self._has_multi_parameters: @@ -556,11 +556,9 @@ class Insert(ValuesBase): raise exc.InvalidRequestError( "This construct already inserts value expressions") - colparams = self._process_colparams(dict((_column_as_key(n), Null()) - for n in names)) - self.parameters = colparams[0] - self._has_multi_parameters = colparams[1] - self._preserve_parameter_order = colparams[2] + self.parameters, self._has_multi_parameters, \ + self._preserve_parameter_order = self._process_colparams( + dict((_column_as_key(n), Null()) for n in names)) self.select_names = names self.inline = True -- cgit v1.2.1