summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql/dml.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/sql/dml.py')
-rw-r--r--lib/sqlalchemy/sql/dml.py537
1 files changed, 341 insertions, 196 deletions
diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py
index 097c513b4..171a2cc2c 100644
--- a/lib/sqlalchemy/sql/dml.py
+++ b/lib/sqlalchemy/sql/dml.py
@@ -8,25 +8,162 @@
Provide :class:`.Insert`, :class:`.Update` and :class:`.Delete`.
"""
-
+from sqlalchemy.types import NullType
from . import coercions
from . import roles
from .base import _from_objects
from .base import _generative
+from .base import CompileState
from .base import DialectKWArgs
from .base import Executable
-from .elements import and_
+from .base import HasCompileState
from .elements import ClauseElement
from .elements import Null
from .selectable import HasCTE
from .selectable import HasPrefixes
+from .visitors import InternalTraversal
from .. import exc
from .. import util
+from ..util import collections_abc
+
+
+class DMLState(CompileState):
+ _no_parameters = True
+ _dict_parameters = None
+ _multi_parameters = None
+ _parameter_ordering = None
+ _has_multi_parameters = False
+ isupdate = False
+ isdelete = False
+ isinsert = False
+
+ def __init__(
+ self,
+ statement,
+ compiler,
+ isinsert=False,
+ isupdate=False,
+ isdelete=False,
+ **kw
+ ):
+ self.statement = statement
+
+ if isupdate:
+ self.isupdate = True
+ self._preserve_parameter_order = (
+ statement._preserve_parameter_order
+ )
+ if statement._ordered_values is not None:
+ self._process_ordered_values(statement)
+ elif statement._values is not None:
+ self._process_values(statement)
+ elif statement._multi_values:
+ self._process_multi_values(statement)
+ self._extra_froms = self._make_extra_froms(statement)
+ elif isinsert:
+ self.isinsert = True
+ if statement._select_names:
+ self._process_select_values(statement)
+ if statement._values is not None:
+ self._process_values(statement)
+ if statement._multi_values:
+ self._process_multi_values(statement)
+ elif isdelete:
+ self.isdelete = True
+ self._extra_froms = self._make_extra_froms(statement)
+ else:
+ assert False, "one of isinsert, isupdate, or isdelete must be set"
+
+ def _make_extra_froms(self, statement):
+ froms = []
+ seen = {statement.table}
+
+ for crit in statement._where_criteria:
+ for item in _from_objects(crit):
+ if not seen.intersection(item._cloned_set):
+ froms.append(item)
+ seen.update(item._cloned_set)
+
+ return froms
+
+ def _process_multi_values(self, statement):
+ if not statement._supports_multi_parameters:
+ raise exc.InvalidRequestError(
+ "%s construct does not support "
+ "multiple parameter sets." % statement.__visit_name__.upper()
+ )
+
+ for parameters in statement._multi_values:
+ multi_parameters = [
+ {
+ c.key: value
+ for c, value in zip(statement.table.c, parameter_set)
+ }
+ if isinstance(parameter_set, collections_abc.Sequence)
+ else parameter_set
+ for parameter_set in parameters
+ ]
+
+ if self._no_parameters:
+ self._no_parameters = False
+ self._has_multi_parameters = True
+ self._multi_parameters = multi_parameters
+ self._dict_parameters = self._multi_parameters[0]
+ elif not self._has_multi_parameters:
+ self._cant_mix_formats_error()
+ else:
+ self._multi_parameters.extend(multi_parameters)
+
+ def _process_values(self, statement):
+ if self._no_parameters:
+ self._has_multi_parameters = False
+ self._dict_parameters = statement._values
+ self._no_parameters = False
+ elif self._has_multi_parameters:
+ self._cant_mix_formats_error()
+
+ def _process_ordered_values(self, statement):
+ parameters = statement._ordered_values
+
+ if self._no_parameters:
+ self._no_parameters = False
+ self._dict_parameters = dict(parameters)
+ self._parameter_ordering = [key for key, value in parameters]
+ elif self._has_multi_parameters:
+ self._cant_mix_formats_error()
+ else:
+ raise exc.InvalidRequestError(
+ "Can only invoke ordered_values() once, and not mixed "
+ "with any other values() call"
+ )
+
+ def _process_select_values(self, statement):
+ parameters = {
+ coercions.expect(roles.DMLColumnRole, name, as_key=True): Null()
+ for name in statement._select_names
+ }
+
+ if self._no_parameters:
+ self._no_parameters = False
+ self._dict_parameters = parameters
+ else:
+ # this condition normally not reachable as the Insert
+ # does not allow this construction to occur
+ assert False, "This statement already has parameters"
+
+ def _cant_mix_formats_error(self):
+ raise exc.InvalidRequestError(
+ "Can't mix single and multiple VALUES "
+ "formats in one INSERT statement; one style appends to a "
+ "list while the other replaces values, so the intent is "
+ "ambiguous."
+ )
class UpdateBase(
roles.DMLRole,
HasCTE,
+ HasCompileState,
DialectKWArgs,
HasPrefixes,
Executable,
@@ -42,10 +179,10 @@ class UpdateBase(
{"autocommit": True}
)
_hints = util.immutabledict()
- _parameter_ordering = None
- _prefixes = ()
named_with_column = False
+ _compile_state_cls = DMLState
+
@classmethod
def _constructor_20_deprecations(cls, fn_name, clsname, names):
@@ -112,43 +249,6 @@ class UpdateBase(
col._make_proxy(fromclause) for col in self._returning
)
- def _process_colparams(self, parameters, preserve_parameter_order=False):
- def process_single(p):
- if isinstance(p, (list, tuple)):
- return dict((c.key, pval) for c, pval in zip(self.table.c, p))
- else:
- return p
-
- if (
- preserve_parameter_order or self._preserve_parameter_order
- ) and parameters is not None:
- if not isinstance(parameters, list) or (
- parameters and not isinstance(parameters[0], tuple)
- ):
- raise ValueError(
- "When preserve_parameter_order is True, "
- "values() only accepts a list of 2-tuples"
- )
- self._parameter_ordering = [key for key, value in parameters]
-
- return dict(parameters), False
-
- if (
- 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
- else:
- return process_single(parameters), False
-
def params(self, *arg, **kw):
"""Set the parameters for the statement.
@@ -163,6 +263,29 @@ class UpdateBase(
" stmt.values(**parameters)."
)
+ @_generative
+ def with_dialect_options(self, **opt):
+ """Add dialect options to this INSERT/UPDATE/DELETE object.
+
+ e.g.::
+
+ upd = table.update().dialect_options(mysql_limit=10)
+
+ .. versionadded: 1.4 - this method supersedes the dialect options
+ associated with the constructor.
+
+
+ """
+ self._validate_dialect_kwargs(opt)
+
+ def _validate_dialect_kwargs_deprecated(self, dialect_kw):
+ util.warn_deprecated_20(
+ "Passing dialect keyword arguments directly to the "
+ "constructor is deprecated and will be removed in SQLAlchemy "
+ "2.0. Please use the ``with_dialect_options()`` method."
+ )
+ self._validate_dialect_kwargs(dialect_kw)
+
def bind(self):
"""Return a 'bind' linked to this :class:`.UpdateBase`
or a :class:`.Table` associated with it.
@@ -266,9 +389,6 @@ class UpdateBase(
self._hints = self._hints.union({(selectable, dialect_name): text})
- def _copy_internals(self, **kw):
- raise NotImplementedError()
-
class ValuesBase(UpdateBase):
"""Supplies support for :meth:`.ValuesBase.values` to
@@ -277,16 +397,21 @@ class ValuesBase(UpdateBase):
__visit_name__ = "values_base"
_supports_multi_parameters = False
- _has_multi_parameters = False
_preserve_parameter_order = False
select = None
_post_values_clause = None
+ _values = None
+ _multi_values = ()
+ _ordered_values = None
+ _select_names = None
+
+ _returning = ()
+
def __init__(self, table, values, prefixes):
self.table = coercions.expect(roles.FromClauseRole, table)
- self.parameters, self._has_multi_parameters = self._process_colparams(
- values
- )
+ if values is not None:
+ self.values.non_generative(self, values)
if prefixes:
self._setup_prefixes(prefixes)
@@ -416,59 +541,96 @@ class ValuesBase(UpdateBase):
:func:`~.expression.update` - produce an ``UPDATE`` statement
"""
- if self.select is not None:
+ if self._select_names:
raise exc.InvalidRequestError(
"This construct already inserts from a SELECT"
)
- if self._has_multi_parameters and kwargs:
- raise exc.InvalidRequestError(
- "This construct already has multiple parameter sets."
+ elif self._ordered_values:
+ raise exc.ArgumentError(
+ "This statement already has ordered values present"
)
if args:
- if len(args) > 1:
+ # positional case. this is currently expensive. we don't
+ # yet have positional-only args so we have to check the length.
+ # then we need to check multiparams vs. single dictionary.
+ # since the parameter format is needed in order to determine
+ # a cache key, we need to determine this up front.
+ arg = args[0]
+
+ if kwargs:
+ raise exc.ArgumentError(
+ "Can't pass positional and kwargs to values() "
+ "simultaneously"
+ )
+ elif len(args) > 1:
raise exc.ArgumentError(
"Only a single dictionary/tuple or list of "
"dictionaries/tuples is accepted positionally."
)
- v = args[0]
- else:
- v = {}
- if self.parameters is None:
- (
- self.parameters,
- self._has_multi_parameters,
- ) = self._process_colparams(v)
- else:
- if self._has_multi_parameters:
- self.parameters = list(self.parameters)
- p, self._has_multi_parameters = 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"
- )
+ elif not self._preserve_parameter_order and isinstance(
+ arg, collections_abc.Sequence
+ ):
- self.parameters.extend(p)
- 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)
+ if arg and isinstance(arg[0], (list, dict, tuple)):
+ self._multi_values += (arg,)
+ return
- if kwargs:
- if self._has_multi_parameters:
+ # tuple values
+ arg = {c.key: value for c, value in zip(self.table.c, arg)}
+ elif self._preserve_parameter_order and not isinstance(
+ arg, collections_abc.Sequence
+ ):
+ raise ValueError(
+ "When preserve_parameter_order is True, "
+ "values() only accepts a list of 2-tuples"
+ )
+
+ else:
+ # kwarg path. this is the most common path for non-multi-params
+ # so this is fairly quick.
+ arg = kwargs
+ if args:
raise exc.ArgumentError(
- "Can't pass kwargs and multiple parameter sets "
- "simultaneously"
+ "Only a single dictionary/tuple or list of "
+ "dictionaries/tuples is accepted positionally."
)
+
+ # for top level values(), convert literals to anonymous bound
+ # parameters at statement construction time, so that these values can
+ # participate in the cache key process like any other ClauseElement.
+ # crud.py now intercepts bound parameters with unique=True from here
+ # and ensures they get the "crud"-style name when rendered.
+
+ if self._preserve_parameter_order:
+ arg = [
+ (
+ k,
+ coercions.expect(
+ roles.ExpressionElementRole,
+ v,
+ type_=NullType(),
+ is_crud=True,
+ ),
+ )
+ for k, v in arg
+ ]
+ self._ordered_values = arg
+ else:
+ arg = {
+ k: coercions.expect(
+ roles.ExpressionElementRole,
+ v,
+ type_=NullType(),
+ is_crud=True,
+ )
+ for k, v in arg.items()
+ }
+ if self._values:
+ self._values = self._values.union(arg)
else:
- self.parameters.update(kwargs)
+ self._values = util.immutabledict(arg)
@_generative
def return_defaults(self, *cols):
@@ -555,6 +717,25 @@ class Insert(ValuesBase):
_supports_multi_parameters = True
+ select = None
+ include_insert_from_select_defaults = False
+
+ _traverse_internals = (
+ [
+ ("table", InternalTraversal.dp_clauseelement),
+ ("_inline", InternalTraversal.dp_boolean),
+ ("_select_names", InternalTraversal.dp_string_list),
+ ("_values", InternalTraversal.dp_dml_values),
+ ("_multi_values", InternalTraversal.dp_dml_multi_values),
+ ("select", InternalTraversal.dp_clauseelement),
+ ("_post_values_clause", InternalTraversal.dp_clauseelement),
+ ("_returning", InternalTraversal.dp_clauseelement_list),
+ ("_hints", InternalTraversal.dp_table_hint_list),
+ ]
+ + HasPrefixes._has_prefixes_traverse_internals
+ + DialectKWArgs._dialect_kwargs_traverse_internals
+ )
+
@ValuesBase._constructor_20_deprecations(
"insert",
"Insert",
@@ -626,18 +807,13 @@ class Insert(ValuesBase):
"""
super(Insert, self).__init__(table, values, prefixes)
self._bind = bind
- self.select = self.select_names = None
- self.include_insert_from_select_defaults = False
self._inline = inline
- self._returning = returning
- self._validate_dialect_kwargs(dialect_kw)
- self._return_defaults = return_defaults
+ if returning:
+ self._returning = returning
+ if dialect_kw:
+ self._validate_dialect_kwargs_deprecated(dialect_kw)
- def get_children(self, **kwargs):
- if self.select is not None:
- return (self.select,)
- else:
- return ()
+ self._return_defaults = return_defaults
@_generative
def inline(self):
@@ -702,25 +878,34 @@ class Insert(ValuesBase):
:attr:`.ResultProxy.inserted_primary_key` accessor does not apply.
"""
- if self.parameters:
+
+ if self._values:
raise exc.InvalidRequestError(
"This construct already inserts value expressions"
)
- self.parameters, self._has_multi_parameters = self._process_colparams(
- {
- coercions.expect(roles.DMLColumnRole, n, as_key=True): Null()
- for n in names
- }
- )
-
- self.select_names = names
+ self._select_names = names
self._inline = True
self.include_insert_from_select_defaults = include_defaults
self.select = coercions.expect(roles.DMLSelectRole, select)
-class Update(ValuesBase):
+class DMLWhereBase(object):
+ _where_criteria = ()
+
+ @_generative
+ def where(self, whereclause):
+ """return a new construct with the given expression added to
+ its WHERE clause, joined to the existing clause via AND, if any.
+
+ """
+
+ self._where_criteria += (
+ coercions.expect(roles.WhereHavingRole, whereclause),
+ )
+
+
+class Update(DMLWhereBase, ValuesBase):
"""Represent an Update construct.
The :class:`.Update` object is created using the :func:`update()`
@@ -730,6 +915,20 @@ class Update(ValuesBase):
__visit_name__ = "update"
+ _traverse_internals = (
+ [
+ ("table", InternalTraversal.dp_clauseelement),
+ ("_where_criteria", InternalTraversal.dp_clauseelement_list),
+ ("_inline", InternalTraversal.dp_boolean),
+ ("_ordered_values", InternalTraversal.dp_dml_ordered_values),
+ ("_values", InternalTraversal.dp_dml_values),
+ ("_returning", InternalTraversal.dp_clauseelement_list),
+ ("_hints", InternalTraversal.dp_table_hint_list),
+ ]
+ + HasPrefixes._has_prefixes_traverse_internals
+ + DialectKWArgs._dialect_kwargs_traverse_internals
+ )
+
@ValuesBase._constructor_20_deprecations(
"update",
"Update",
@@ -874,21 +1073,14 @@ class Update(ValuesBase):
self._bind = bind
self._returning = returning
if whereclause is not None:
- self._whereclause = coercions.expect(
- roles.WhereHavingRole, whereclause
+ self._where_criteria += (
+ coercions.expect(roles.WhereHavingRole, whereclause),
)
- else:
- self._whereclause = None
self._inline = inline
- self._validate_dialect_kwargs(dialect_kw)
+ if dialect_kw:
+ self._validate_dialect_kwargs_deprecated(dialect_kw)
self._return_defaults = return_defaults
- def get_children(self, **kwargs):
- if self._whereclause is not None:
- return (self._whereclause,)
- else:
- return ()
-
@_generative
def ordered_values(self, *args):
"""Specify the VALUES clause of this UPDATE statement with an explicit
@@ -912,22 +1104,27 @@ class Update(ValuesBase):
parameter, which will be removed in SQLAlchemy 2.0.
"""
- if self.select is not None:
- raise exc.InvalidRequestError(
- "This construct already inserts from a SELECT"
- )
-
- if self.parameters is None:
- (
- self.parameters,
- self._has_multi_parameters,
- ) = self._process_colparams(
- list(args), preserve_parameter_order=True
- )
- else:
+ if self._values:
raise exc.ArgumentError(
"This statement already has values present"
)
+ elif self._ordered_values:
+ raise exc.ArgumentError(
+ "This statement already has ordered values present"
+ )
+ arg = [
+ (
+ k,
+ coercions.expect(
+ roles.ExpressionElementRole,
+ v,
+ type_=NullType(),
+ is_crud=True,
+ ),
+ )
+ for k, v in args
+ ]
+ self._ordered_values = arg
@_generative
def inline(self):
@@ -945,37 +1142,8 @@ class Update(ValuesBase):
"""
self._inline = True
- @_generative
- def where(self, whereclause):
- """return a new update() construct with the given expression added to
- its WHERE clause, joined to the existing clause via AND, if any.
-
- """
- if self._whereclause is not None:
- self._whereclause = and_(
- self._whereclause,
- coercions.expect(roles.WhereHavingRole, whereclause),
- )
- else:
- self._whereclause = coercions.expect(
- roles.WhereHavingRole, whereclause
- )
-
- @property
- def _extra_froms(self):
- froms = []
- seen = {self.table}
-
- if self._whereclause is not None:
- for item in _from_objects(self._whereclause):
- if not seen.intersection(item._cloned_set):
- froms.append(item)
- seen.update(item._cloned_set)
-
- return froms
-
-class Delete(UpdateBase):
+class Delete(DMLWhereBase, UpdateBase):
"""Represent a DELETE construct.
The :class:`.Delete` object is created using the :func:`delete()`
@@ -985,6 +1153,17 @@ class Delete(UpdateBase):
__visit_name__ = "delete"
+ _traverse_internals = (
+ [
+ ("table", InternalTraversal.dp_clauseelement),
+ ("_where_criteria", InternalTraversal.dp_clauseelement_list),
+ ("_returning", InternalTraversal.dp_clauseelement_list),
+ ("_hints", InternalTraversal.dp_table_hint_list),
+ ]
+ + HasPrefixes._has_prefixes_traverse_internals
+ + DialectKWArgs._dialect_kwargs_traverse_internals
+ )
+
@ValuesBase._constructor_20_deprecations(
"delete",
"Delete",
@@ -1041,43 +1220,9 @@ class Delete(UpdateBase):
self._setup_prefixes(prefixes)
if whereclause is not None:
- self._whereclause = coercions.expect(
- roles.WhereHavingRole, whereclause
- )
- else:
- self._whereclause = None
-
- self._validate_dialect_kwargs(dialect_kw)
-
- def get_children(self, **kwargs):
- if self._whereclause is not None:
- return (self._whereclause,)
- else:
- return ()
-
- @_generative
- def where(self, whereclause):
- """Add the given WHERE clause to a newly returned delete construct."""
-
- if self._whereclause is not None:
- self._whereclause = and_(
- self._whereclause,
+ self._where_criteria += (
coercions.expect(roles.WhereHavingRole, whereclause),
)
- else:
- self._whereclause = coercions.expect(
- roles.WhereHavingRole, whereclause
- )
-
- @property
- def _extra_froms(self):
- froms = []
- seen = {self.table}
- if self._whereclause is not None:
- for item in _from_objects(self._whereclause):
- if not seen.intersection(item._cloned_set):
- froms.append(item)
- seen.update(item._cloned_set)
-
- return froms
+ if dialect_kw:
+ self._validate_dialect_kwargs_deprecated(dialect_kw)