summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-06-23 16:21:04 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-06-25 18:58:34 -0400
commitf1a3038f480ee1965928cdcd1dc0c47347f270bc (patch)
tree8b03334c438631e72f132533db676b3bf25a3f00 /lib/sqlalchemy/sql
parent660a340bff8fcefd2826032e75210c0924a2335e (diff)
downloadsqlalchemy-f1a3038f480ee1965928cdcd1dc0c47347f270bc.tar.gz
Default psycopg2 executemany mode to "values_only"
The psycopg2 dialect now defaults to using the very performant ``execute_values()`` psycopg2 extension for compiled INSERT statements, and also impements RETURNING support when this extension is used. This allows INSERT statements that even include an autoincremented SERIAL or IDENTITY value to run very fast while still being able to return the newly generated primary key values. The ORM will then integrate this new feature in a separate change. Implements RETURNING for insert with executemany Adds support to return_defaults() mode and inserted_primary_key to support mutiple INSERTed rows, via return_defauls_rows and inserted_primary_key_rows accessors. within default execution context, new cached compiler getters are used to fetch primary keys from rows inserted_primary_key now returns a plain tuple. this is not yet a row-like object however this can be added. Adds distinct "values_only" and "batch" modes, as "values" has a lot of benefits but "batch" breaks cursor.rowcount psycopg2 minimum version 2.7 so we can remove the large number of checks for very old versions of psycopg2 simplify tests to no longer distinguish between native and non-native json Fixes: #5401 Change-Id: Ic08fd3423d4c5d16ca50994460c0c234868bd61c
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/compiler.py94
-rw-r--r--lib/sqlalchemy/sql/crud.py9
-rw-r--r--lib/sqlalchemy/sql/dml.py36
-rw-r--r--lib/sqlalchemy/sql/elements.py16
4 files changed, 122 insertions, 33 deletions
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index 61178291a..e09e60c2c 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -676,13 +676,15 @@ class SQLCompiler(Compiled):
"""
+ inline = False
+
def __init__(
self,
dialect,
statement,
cache_key=None,
column_keys=None,
- inline=False,
+ for_executemany=False,
linting=NO_LINTING,
**kwargs
):
@@ -695,8 +697,13 @@ class SQLCompiler(Compiled):
:param column_keys: a list of column names to be compiled into an
INSERT or UPDATE statement.
- :param inline: whether to generate INSERT statements as "inline", e.g.
- not formatted to return any generated defaults
+ :param for_executemany: whether INSERT / UPDATE statements should
+ expect that they are to be invoked in an "executemany" style,
+ which may impact how the statement will be expected to return the
+ values of defaults and autoincrement / sequences and similar.
+ Depending on the backend and driver in use, support for retreiving
+ these values may be disabled which means SQL expressions may
+ be rendered inline, RETURNING may not be rendered, etc.
:param kwargs: additional keyword arguments to be consumed by the
superclass.
@@ -709,9 +716,10 @@ class SQLCompiler(Compiled):
if cache_key:
self._cache_key_bind_match = {b: b for b in cache_key[1]}
- # compile INSERT/UPDATE defaults/sequences inlined (no pre-
- # execute)
- self.inline = inline or getattr(statement, "_inline", False)
+ # compile INSERT/UPDATE defaults/sequences to expect executemany
+ # style execution, which may mean no pre-execute of defaults,
+ # or no RETURNING
+ self.for_executemany = for_executemany
self.linting = linting
@@ -755,10 +763,21 @@ class SQLCompiler(Compiled):
Compiled.__init__(self, dialect, statement, **kwargs)
- if (
- self.isinsert or self.isupdate or self.isdelete
- ) and statement._returning:
- self.returning = statement._returning
+ if self.isinsert or self.isupdate or self.isdelete:
+ if statement._returning:
+ self.returning = statement._returning
+
+ if self.isinsert or self.isupdate:
+ if statement._inline:
+ self.inline = True
+ elif self.for_executemany and (
+ not self.isinsert
+ or (
+ self.dialect.insert_executemany_returning
+ and statement._return_defaults
+ )
+ ):
+ self.inline = True
if self.positional and self._numeric_binds:
self._apply_numbered_params()
@@ -1088,6 +1107,61 @@ class SQLCompiler(Compiled):
self._result_columns
)
+ @util.memoized_property
+ def _inserted_primary_key_from_lastrowid_getter(self):
+ key_getter = self._key_getters_for_crud_column[2]
+ table = self.statement.table
+
+ getters = [
+ (operator.methodcaller("get", key_getter(col), None), col)
+ for col in table.primary_key
+ ]
+
+ autoinc_col = table._autoincrement_column
+ if autoinc_col is not None:
+ # apply type post processors to the lastrowid
+ proc = autoinc_col.type._cached_result_processor(
+ self.dialect, None
+ )
+ else:
+ proc = None
+
+ def get(lastrowid, parameters):
+ if proc is not None:
+ lastrowid = proc(lastrowid)
+
+ if lastrowid is None:
+ return tuple(getter(parameters) for getter, col in getters)
+ else:
+ return tuple(
+ lastrowid if col is autoinc_col else getter(parameters)
+ for getter, col in getters
+ )
+
+ return get
+
+ @util.memoized_property
+ def _inserted_primary_key_from_returning_getter(self):
+ key_getter = self._key_getters_for_crud_column[2]
+ table = self.statement.table
+
+ ret = {col: idx for idx, col in enumerate(self.returning)}
+
+ getters = [
+ (operator.itemgetter(ret[col]), True)
+ if col in ret
+ else (operator.methodcaller("get", key_getter(col), None), False)
+ for col in table.primary_key
+ ]
+
+ def get(row, parameters):
+ return tuple(
+ getter(row) if use_row else getter(parameters)
+ for getter, use_row in getters
+ )
+
+ return get
+
def default_from(self):
"""Called when a SELECT statement has no froms, and no FROM clause is
to be appended.
diff --git a/lib/sqlalchemy/sql/crud.py b/lib/sqlalchemy/sql/crud.py
index 625183db3..c80d95a2c 100644
--- a/lib/sqlalchemy/sql/crud.py
+++ b/lib/sqlalchemy/sql/crud.py
@@ -781,7 +781,14 @@ def _get_returning_modifiers(compiler, stmt, compile_state):
need_pks = (
compile_state.isinsert
- and not compiler.inline
+ and not stmt._inline
+ and (
+ not compiler.for_executemany
+ or (
+ compiler.dialect.insert_executemany_returning
+ and stmt._return_defaults
+ )
+ )
and not stmt._returning
and not compile_state._has_multi_parameters
)
diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py
index 50b2a935a..4e8430450 100644
--- a/lib/sqlalchemy/sql/dml.py
+++ b/lib/sqlalchemy/sql/dml.py
@@ -693,18 +693,18 @@ class ValuesBase(UpdateBase):
This method differs from :meth:`.UpdateBase.returning` in these ways:
- 1. :meth:`.ValuesBase.return_defaults` is only intended for use with
- an INSERT or an UPDATE statement that matches exactly one row.
- While the RETURNING construct in the general sense supports
- multiple rows for a multi-row UPDATE or DELETE statement, or for
- special cases of INSERT that return multiple rows (e.g. INSERT from
- SELECT, multi-valued VALUES clause),
+ 1. :meth:`.ValuesBase.return_defaults` is only intended for use with an
+ INSERT or an UPDATE statement that matches exactly one row per
+ parameter set. While the RETURNING construct in the general sense
+ supports multiple rows for a multi-row UPDATE or DELETE statement,
+ or for special cases of INSERT that return multiple rows (e.g.
+ INSERT from SELECT, multi-valued VALUES clause),
:meth:`.ValuesBase.return_defaults` is intended only for an
- "ORM-style" single-row INSERT/UPDATE statement. The row returned
- by the statement is also consumed implicitly when
+ "ORM-style" single-row INSERT/UPDATE statement. The row
+ returned by the statement is also consumed implicitly when
:meth:`.ValuesBase.return_defaults` is used. By contrast,
- :meth:`.UpdateBase.returning` leaves the RETURNING result-set
- intact with a collection of any number of rows.
+ :meth:`.UpdateBase.returning` leaves the RETURNING result-set intact
+ with a collection of any number of rows.
2. It is compatible with the existing logic to fetch auto-generated
primary key values, also known as "implicit returning". Backends
@@ -718,6 +718,16 @@ class ValuesBase(UpdateBase):
an exception. The return value of
:attr:`_engine.CursorResult.returned_defaults` will be ``None``
+ 4. An INSERT statement invoked with executemany() is supported if the
+ backend database driver supports the
+ ``insert_executemany_returning`` feature, currently this includes
+ PostgreSQL with psycopg2. When executemany is used, the
+ :attr:`_engine.CursorResult.returned_defaults_rows` and
+ :attr:`_engine.CursorResult.inserted_primary_key_rows` accessors
+ will return the inserted defaults and primary keys.
+
+ .. versionadded:: 1.4
+
:meth:`.ValuesBase.return_defaults` is used by the ORM to provide
an efficient implementation for the ``eager_defaults`` feature of
:func:`.mapper`.
@@ -735,6 +745,12 @@ class ValuesBase(UpdateBase):
:attr:`_engine.CursorResult.returned_defaults`
+ :attr:`_engine.CursorResult.returned_defaults_rows`
+
+ :attr:`_engine.CursorResult.inserted_primary_key`
+
+ :attr:`_engine.CursorResult.inserted_primary_key_rows`
+
"""
self._return_defaults = cols or True
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 60c816ee6..5cb411b85 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -482,14 +482,6 @@ class ClauseElement(
's bound engine,
if any.
- :param inline: Used for INSERT statements, for a dialect which does
- not support inline retrieval of newly generated primary key
- columns, will force the expression used to create the new primary
- key value to be rendered inline within the INSERT statement's
- VALUES clause. This typically refers to Sequence execution but may
- also refer to any server-side default generation function
- associated with a primary key `Column`.
-
:param compile_kwargs: optional dictionary of additional parameters
that will be passed through to the compiler within all "visit"
methods. This allows any custom flag to be passed through to
@@ -528,7 +520,7 @@ class ClauseElement(
dialect,
compiled_cache=None,
column_keys=None,
- inline=False,
+ for_executemany=False,
schema_translate_map=None,
**kw
):
@@ -546,7 +538,7 @@ class ClauseElement(
cache_key,
tuple(column_keys),
bool(schema_translate_map),
- inline,
+ for_executemany,
)
compiled_sql = compiled_cache.get(key)
@@ -555,7 +547,7 @@ class ClauseElement(
dialect,
cache_key=elem_cache_key,
column_keys=column_keys,
- inline=inline,
+ for_executemany=for_executemany,
schema_translate_map=schema_translate_map,
**kw
)
@@ -568,7 +560,7 @@ class ClauseElement(
dialect,
cache_key=elem_cache_key,
column_keys=column_keys,
- inline=inline,
+ for_executemany=for_executemany,
schema_translate_map=schema_translate_map,
**kw
)