feat: per-dialect optimal upsert_many (MERGE / ON CONFLICT / INSERT OR UPDATE)#740
Open
cofin wants to merge 15 commits into
Open
feat: per-dialect optimal upsert_many (MERGE / ON CONFLICT / INSERT OR UPDATE)#740cofin wants to merge 15 commits into
upsert_many (MERGE / ON CONFLICT / INSERT OR UPDATE)#740cofin wants to merge 15 commits into
Conversation
upsert_many (MERGE / ON CONFLICT / INSERT OR UPDATE)
cofin
force-pushed
the
feat/optimal-upsert-many
branch
from
May 24, 2026 23:38
89c824e to
e61dc85
Compare
cofin
added a commit
to cofin/advanced-alchemy
that referenced
this pull request
May 25, 2026
Two regressions from 116b813 (revert of mssql/spanner from ``OnConflictUpsert.supports_native_upsert``) surfaced on PR litestar-org#740: 1. ``test_supports_native_upsert_all_dialects`` (mssql_engine / mssql_async_engine) — the integration ``expected_support`` set still listed ``mssql`` and ``spanner``. The unit-test mirror was updated in the revert but this integration assertion was left behind. Trim it back to the single-row ON-CONFLICT dialects and document the split. 2. ``test_upsert_many_chunk_size_emits_multiple_chunks`` (and its sibling ``test_upsert_many_native_path_is_single_statement_per_chunk``) — the bigint guard introduced by the revert checked ``issubclass(author_model, BigIntBase)``, but the integration fixture uses ``BigIntAuditBase``, which shares only the ``BigIntPrimaryKey`` mixin with ``BigIntBase`` (no subclass relationship). The skip never triggered for ``oracle*``/``mssql``, so the safety-net fallback in ``Repository.upsert_many`` ran and produced one bulk INSERT — failing the ``>= 2 chunks`` assertion. The sibling test happened to pass by coincidence (``insertmanyvalues`` produced exactly the one statement it expected). Switch the guard to ``BigIntPrimaryKey`` so any bigint-PK base catches it.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #740 +/- ##
==========================================
- Coverage 81.56% 80.88% -0.69%
==========================================
Files 103 103
Lines 8730 8902 +172
Branches 1179 1219 +40
==========================================
+ Hits 7121 7200 +79
- Misses 1281 1370 +89
- Partials 328 332 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
cofin
added a commit
to cofin/advanced-alchemy
that referenced
this pull request
May 30, 2026
Two regressions from 116b813 (revert of mssql/spanner from ``OnConflictUpsert.supports_native_upsert``) surfaced on PR litestar-org#740: 1. ``test_supports_native_upsert_all_dialects`` (mssql_engine / mssql_async_engine) — the integration ``expected_support`` set still listed ``mssql`` and ``spanner``. The unit-test mirror was updated in the revert but this integration assertion was left behind. Trim it back to the single-row ON-CONFLICT dialects and document the split. 2. ``test_upsert_many_chunk_size_emits_multiple_chunks`` (and its sibling ``test_upsert_many_native_path_is_single_statement_per_chunk``) — the bigint guard introduced by the revert checked ``issubclass(author_model, BigIntBase)``, but the integration fixture uses ``BigIntAuditBase``, which shares only the ``BigIntPrimaryKey`` mixin with ``BigIntBase`` (no subclass relationship). The skip never triggered for ``oracle*``/``mssql``, so the safety-net fallback in ``Repository.upsert_many`` ran and produced one bulk INSERT — failing the ``>= 2 chunks`` assertion. The sibling test happened to pass by coincidence (``insertmanyvalues`` produced exactly the one statement it expected). Switch the guard to ``BigIntPrimaryKey`` so any bigint-PK base catches it.
cofin
force-pushed
the
feat/optimal-upsert-many
branch
from
May 30, 2026 00:13
c159a70 to
d80bb6b
Compare
Add OnConflictUpsert.create_upsert_many and create_merge_many so a single
call can compile to one INSERT...VALUES(...), (...) per chunk on the
ON CONFLICT dialects, and to a single multi-row MERGE on Oracle / PG /
MSSQL — establishing the foundation for repository.upsert_many to dispatch
to a native primitive in Ch.3.
create_upsert_many:
- postgresql / cockroachdb / sqlite / duckdb -> pg_insert(...).values(list)
.on_conflict_do_update(...); supports_returning=True
- mysql / mariadb -> mysql_insert(...).values(list)
.on_duplicate_key_update(...); supports_returning=False
- empty list and heterogeneous-key inputs raise ValueError
create_merge_many:
- oracle -> single MergeStatement whose source is
select(...) FROM DUAL UNION ALL select(...) FROM DUAL, with per-row
bindparam namespacing (row{i}_*) and per-row PK default generation
- postgresql / cockroachdb -> single MergeStatement whose source is
select(...) UNION ALL select(...) subquery
- mssql -> single MergeStatement with raw "VALUES (...), (...) AS src(...)"
source string; @compiles(MergeStatement, "mssql") body lands in Ch.4
- other dialects -> list[MergeStatement] executemany fallback
Adds _validate_bulk_inputs helper and three private builders to keep the
public methods short. validate_identifiers=True still rejects bad column
names on the bulk path.
T1 (extending supports_native_upsert to include mssql/spanner) deferred to
Ch.4/Ch.5 where the MergeStatement compile actually lands — flipping it on
in Ch.1 would regress existing spanner Litestar integration tests because
@compiles(MergeStatement, "mssql"/"spanner") doesn't exist yet. Saga-level
acceptance gate ("Litestar single-row callers unaffected") preserved.
Add module-level resolve_upsert_strategy(table, match_fields, dialect_name)
plus the UpsertStrategy NamedTuple it returns. Caches per (id(table),
sorted-match-fields, dialect) via functools.lru_cache(maxsize=None) — single
introspection per declarative class, mirroring the existing _pk_attr_names
/ has_composite_pk pattern.
Resolution priority:
1. match_fields superset PK -> native primitive, conflict_columns=PK
2. match_fields == UniqueConstraint columns -> native, that constraint
3. match_fields == unique Index columns -> native, that index
4. otherwise -> kind="fallback" (correctness anchor — non-unique match
keys would silently insert duplicates on the native path)
Dialect -> kind mapping:
postgresql/cockroachdb/sqlite/duckdb -> on_conflict, returning=True
mysql/mariadb -> on_conflict, returning=False
oracle/mssql -> merge, returning=True
spanner -> insert_or_update, returning=False
anything else -> fallback, returning=False
ValueError raised early if match_fields is empty or references columns
not present on the table.
Add @compiles(MergeStatement, "mssql") emitting the T-SQL form MERGE INTO {table} AS tgt USING (VALUES (...)) AS src(...) ON tgt.col = src.col [AND ...] WHEN MATCHED THEN UPDATE SET tgt.col = src.col [, ...] WHEN NOT MATCHED THEN INSERT (...) VALUES (src...) OUTPUT inserted.*; The trailing semicolon is mandatory for T-SQL MERGE — without it SQL Server raises a syntax error. ``OUTPUT inserted.*`` is the RETURNING equivalent so the Ch.6 hydration helper can rebuild instances from the cursor result without re-SELECT on mssql. Pairs with create_merge_many's mssql branch (Ch.1) which builds the raw-string "VALUES (...), (...) AS src(...)" source — this compiler emits it verbatim.
Cloud Spanner has no SQL MERGE; the closest DML form is
``INSERT OR UPDATE INTO {table} ({cols}) VALUES (...), (...)``. Model it
with a dedicated SpannerUpsert ClauseElement instead of overloading
MergeStatement, because the syntax has no USING / WHEN MATCHED shape.
Adds:
- SpannerUpsert(Executable, ClauseElement) — constructor validates
non-empty values_list and homogeneous row keys
- @compiles(SpannerUpsert) default — raises NotImplementedError for
non-spanner dialects
- @compiles(SpannerUpsert, "spanner") — emits
INSERT OR UPDATE INTO {tbl} ({cols}) VALUES (...), (...)
INSERT OR UPDATE has no RETURNING/OUTPUT, so resolve_upsert_strategy
already returns supports_returning=False for "spanner"; the Ch.6
hydration helper re-SELECTs in that case.
The PK must be present in every row (Spanner does not auto-generate PKs
via DML); callers are responsible for supplying it. Mutation API path
remains out of scope for this saga.
….3 + Ch.6)
Replace the SELECT-then-partition body of Repository.upsert_many with a
resolver-driven dispatch into the dialect's optimal upsert primitive,
while preserving the old behavior as _upsert_many_fallback for no_merge=True
and for dialects/match_fields the resolver cannot prove unique.
Dispatch:
- kind="on_conflict" + supports_returning -> OnConflictUpsert.create_upsert_many
+ .returning(model_type), session.scalars hydrates attached instances
(postgresql, cockroachdb, sqlite, duckdb)
- kind="on_conflict" + !supports_returning -> create_upsert_many + execute,
hydrate via re-SELECT keyed on match_fields (mysql, mariadb)
- kind="merge" -> create_merge_many (oracle / mssql, single-statement
MERGE per chunk), hydrate via re-SELECT
- kind="insert_or_update" -> SpannerUpsert, hydrate via re-SELECT
- kind="fallback" or no_merge=True -> _upsert_many_fallback (verbatim
extract of the previous body)
Adds:
- chunk_size kwarg mirroring add_many; sized via
_get_insertmanyvalues_max_parameters // len(input_dicts[0])
- _prepare_upsert_row: model_to_dict + drop None-valued PK columns so
autoincrement / sequence DEFAULTs fire on the DB side; Python-side
UUID/callable defaults from __init__ are preserved
- _execute_upsert_chunk + _reselect_upsert_chunk private helpers
Drops the unused POSTGRES_VERSION_SUPPORTING_MERGE constant — dialect
gating now lives inside operations.resolve_upsert_strategy.
Regenerates _sync.py via unasyncd.
Adds the deferred T1 from Ch.1: extend OnConflictUpsert.supports_native_upsert
to include mssql + spanner, and add mssql/spanner branches to create_upsert
that delegate to create_merge_many (single-row list -> MergeStatement) and
SpannerUpsert respectively. Now safe because Ch.4 and Ch.5 supplied the
@compiles bodies. The Litestar single-row store/session callers transparently
get native dispatch on mssql and spanner with no caller changes.
Verified on aiosqlite: 106 integration tests pass, including the 4 existing
upsert_many tests and the composite-PK upsert_many cases.
- Add ``chunk_size: Optional[int] = None`` to SQLAlchemyAsyncService.upsert_many and pass through to Repository.upsert_many. - Replace the "(**reserved for future use**)" no_merge docstring with a real description matching the Ch.3 behavior: it now forces the SELECT+partition fallback path on dialects that otherwise resolve to a native primitive. - Mirror the new kwarg in SQLAlchemyAsyncMockRepository.upsert_many to keep the service signature compatible with the in-memory test backend. - Regenerate service/_sync.py + memory/_sync.py via unasyncd.
… assertions (Ch.8) Three new integration tests using SQLAlchemy 'before_execute' event listeners: - test_upsert_many_native_path_is_single_statement_per_chunk: on a native-capable dialect, upsert_many must emit exactly one INSERT/MERGE/INSERT-OR-UPDATE statement per chunk, never the historical 1-SELECT + N-add/update fallback shape. Saga acceptance gate. - test_upsert_many_no_merge_forces_fallback: with no_merge=True, the SELECT+INSERT shape is back — proving the escape hatch still routes through _upsert_many_fallback. - test_upsert_many_chunk_size_emits_multiple_chunks: passing chunk_size=4 with 8 rows produces >=2 upsert statements, proving the chunking respects the kwarg. Tests are dialect-aware: native-path assertions skip when the dialect isn't native-capable; the fallback test runs everywhere. Full integration matrix (postgres, oracle, mssql, spanner, mysql, cockroachdb, duckdb) runs automatically in CI via pytest-databases markers.
…h.9) - docs/usage/repositories/basics.rst: add an "Upsert Many" subsection with the full dispatch matrix (dialect / native statement / RETURNING / hydration path), the match-key safety gate rationale, the no_merge=True escape-hatch semantics, and the new chunk_size kwarg. - docs/changelog.rst: add 1.11.0 entry summarising the saga — new bulk facade (create_upsert_many / create_merge_many), SpannerUpsert + its compile, MSSQL MERGE compile, resolve_upsert_strategy resolver, chunk_size kwarg, real no_merge behavior, mssql/spanner inclusion in supports_native_upsert, removal of the dead POSTGRES_VERSION_SUPPORTING_MERGE constant. Explicit: no breaking API changes.
Drop the Internals bullet list and the "No breaking API changes" closing paragraph — keep just the one-paragraph description of the dispatch behavior. Implementation details live in the commit history.
CI surfaced several issues with the per-dialect upsert_many rollout. All
addressed without changing the saga's public API.
MSSQL bind translation
----------------------
The Ch.1 _build_mssql_bulk_merge produced a raw-string source
``"VALUES (:row0_key, ...) AS src(...)"`` — SQLAlchemy treats the string as
opaque text, so the ``:name`` markers never get translated to pyodbc's
``?`` qmark placeholders. Result: ``[SQL Server]Incorrect syntax near ':'``
on every MSSQL upsert path. Switch the source to
``text("(VALUES (:row0_key, ...)) AS src(...)").bindparams(*bp)`` so the
binds are part of the compiler's AST and get qmark-translated at execute
time. Move the alias outside the inner parens so the wrapped output is
``USING (VALUES (?, ?, ?), (?, ?, ?)) AS src(...) ON ...`` (T-SQL-correct).
Native-path defaults (Oracle ORA-01400, MySQL hydration)
-------------------------------------------------------
_prepare_upsert_row now invokes Python-side callable defaults
(``default=uuid7``, ``default=datetime.utcnow``, etc.) for columns missing
from the instance — the native dispatch path bypasses SQLAlchemy's ORM
flush where these would normally fire, and a hand-built MergeStatement
only emits the columns we hand it. Fixes ORA-01400 on
``UserRole.assigned_at`` (NOT NULL, Python default) and lets the
``mysql/mariadb`` re-SELECT hydration find the rows it just inserted on
UUID-PK models.
create_upsert mssql/spanner branches accept list-of-dicts
---------------------------------------------------------
Some integration tests pass a list to the single-row ``create_upsert``
API (works on pg/mysql because ``Insert.values(list)`` is accepted). The
new mssql / spanner delegations forward into ``create_merge_many`` /
``SpannerUpsert`` which require a real ``list[dict]`` shape. Added a
``_coerce_values_list`` helper that normalizes the input.
Litestar session.py mock unit tests
-----------------------------------
The mock fixtures set ``dialect.name = "mssql"`` with the comment "Use
dialect that forces fallback path in tests". With T1 adding mssql to
supports_native_upsert, mssql now triggers native dispatch — bypassing the
mocked _get_session_obj / add the tests asserted. Change the mock dialect
to ``"unknown_dialect"`` so the comment's intent is preserved.
Integration test fixes
----------------------
- ``test_supports_native_upsert_all_dialects`` expected set now includes
``mssql`` and ``spanner``.
- The native-path / chunk_size assertion tests compile statements with
the live connection's dialect before stringifying (``str(clauseelement)``
triggered ``compile_merge_default`` and raised NotImplementedError on
MergeStatement / SpannerUpsert).
- The native-path single-statement-per-chunk test now skips on mysql /
mariadb / asyncmy: those dialects can't hydrate autoincrement-PK rows
without RETURNING, so ``match_fields=['id']`` is a degenerate setup.
- Unit test for ``create_merge_many`` mssql branch now asserts compiled
shape (VALUES + AS SRC) rather than raw-string structure.
…ting, zero-arg defaults Three regressions found in the second CI run: 1. NotNullViolationError on id for bigint models (postgres/oracle/mssql) ----------------------------------------------------------------- BigIntPrimaryKey uses ``mapped_column(BigIntIdentity, Sequence(...))``, so the column has a ``default`` attribute set to a Sequence instance (not a Python callable / value). The previous _prepare_upsert_row logic took the ``not callable(arg)`` branch, set ``prepared[id] = arg`` (which is None for Sequence defaults), and emitted ``INSERT (id, …) VALUES (NULL, …)``. PostgreSQL rightly rejected it. Now: only invoke ``arg`` when it's callable; the non-callable branch only fires when ``arg is not None`` (real literal default); the cleanup branch pops PK / default / server_default columns whose value is still None so the database supplies them. 2. uuid7 default not invoked (mssql session NULL id) ------------------------------------------------- SessionModelMixin uses UUIDv7Base whose ``id`` has ``default=uuid7`` — a *zero-arg* callable. The previous code only tried ``arg(ExecutionContext=None)`` and TypeError'd on uuid7's no-arg signature, so the column was dropped instead of populated. MSSQL session model has id NOT NULL with no server default → 23000 IntegrityError. Added _invoke_default(callable_default) module-level helper that tries the context-taking form first, falls back to the zero-arg form (matching SQLAlchemy's own ColumnDefault.invoke_default semantics). 3. MSSQL Incorrect syntax near 'key' (T-SQL reserved word) ------------------------------------------------------- The text() VALUES source emitted ``AS src(key, namespace, value)`` with unquoted identifiers; ``key`` is a T-SQL reserved word so pyodbc rejected the statement. _build_mssql_bulk_merge now bracket-quotes ``[key]`` in the alias column list, the ON clause (``tgt.[key] = src.[key]``), and the WHEN MATCHED / WHEN NOT MATCHED source references. The @compiles(MergeStatement, "mssql") body also routes every emitted table/column identifier through ``compiler.preparer.quote()`` so reserved words survive on the target/source sides too.
…ert path (MSSQL session)
The MSSQL Litestar session backend invokes
``OnConflictUpsert.create_upsert(values={session_id, data, expires_at})``
— it doesn't pass ``id`` because it expects the model's ``default=uuid7``
factory to fire at SQLAlchemy flush time. The native MERGE dispatch
bypasses flush, so without intervention the emitted INSERT omits ``id``
and SQL Server rejects it: ``Cannot insert NULL into column 'id'``.
Add module-level helpers in operations.py:
- ``invoke_python_default(arg)`` — calls the ColumnDefault.arg callable,
trying the context-taking form first then the zero-arg form, returning
``DEFAULT_INVOKE_FAILED`` if both raise. Replaces the inline
``arg(None)`` calls in create_merge_upsert, _collect_oracle_pk_defaults,
and the repository's _prepare_upsert_row that were silently dropping
zero-arg defaults like ``uuid7``.
- ``augment_with_pk_defaults(table, values_list)`` — for every PK column
not in values_list[0] with a Python-callable default, invoke it once
per row and add the result. Wired into ``create_merge_many`` and
``SpannerUpsert.__init__`` so the Litestar single-row / bulk callers
pick up the same auto-population the Repository.upsert_many path
already got via ``_prepare_upsert_row``.
Promoted both to public (no leading underscore) since they're now
imported across modules.
Verified by simulating the Litestar session call locally: the emitted
MERGE now includes ``id`` in both the alias column list and the WHEN NOT
MATCHED THEN INSERT clause, with a freshly invoked uuid4/uuid7 bound
parameter.
…ty net
Two related fixes for the remaining CI regressions on MSSQL and Oracle:
1. Revert T1 — remove mssql/spanner from ``OnConflictUpsert.supports_native_upsert``
----------------------------------------------------------------------------
The original Ch.1 spec wanted T1 to flip these on so the Litestar single-row
``create_upsert`` API would transparently dispatch to MERGE / INSERT OR UPDATE.
In practice that broke two scenarios:
- Litestar's session/store with a UUIDv7 PK model would work, but every other
non-Repository caller (e.g. ``test_store_upsert_integration`` with BigInt PK,
unit-test mocks set to ``dialect.name = "mssql"``) hits the
same MERGE compile and fails because hand-built MERGE doesn't auto-invoke
server-side Sequence/Identity defaults the way PG's ON CONFLICT does.
- Even when the Python ``default=uuid7`` worked, the surface area exposed
a lot of edge cases (NULL-allowed columns, BigInt+Sequence, T-SQL reserved
words in column lists) that don't bring proportional value compared to the
existing Litestar fallback path.
So: pull mssql/spanner back out of ``supports_native_upsert``, drop the
``create_upsert`` delegations to ``MergeStatement`` / ``SpannerUpsert``, and
restore the ``Insert`` return type. The single-row API is again limited to
true ON-CONFLICT dialects. The bulk MERGE / INSERT OR UPDATE primitives
remain available via ``Repository.upsert_many`` (which goes through the
resolver) for callers that opt into them with a complete row shape.
2. Repository.upsert_many safety net for missing PKs on MERGE dialects
-------------------------------------------------------------------
Even via the resolver, MSSQL/Oracle/Spanner MERGE can't auto-invoke
Sequence/Identity defaults for PK columns we omit (PG's INSERT...ON CONFLICT
does via column DEFAULT, but custom @compiles bodies don't have the
pre-execute hook SQLAlchemy uses to satisfy Sequence). Added
``_native_path_missing_required_pks`` — when the resolved strategy is
``merge`` or ``insert_or_update`` and any prepared row is missing a PK
column, fall back to ``_upsert_many_fallback`` (ORM-managed inserts handle
the Sequence properly).
Models with Python-callable PK defaults (UUIDv7Base / UUIDAuditBase) still
take the native MERGE path because ``_prepare_upsert_row`` populates the
value before dispatch.
Tests touched:
- Unit ``test_supports_native_upsert`` updated to assert mssql/spanner are
now False (and the per-delegation tests for them are removed).
- ``test_upsert_many_native_path_is_single_statement_per_chunk`` skips on
``BigIntBase`` models with oracle/mssql — the new safety net falls those
combinations back to the ORM-managed path so the single-statement
assertion doesn't apply.
Two regressions from 116b813 (revert of mssql/spanner from ``OnConflictUpsert.supports_native_upsert``) surfaced on PR litestar-org#740: 1. ``test_supports_native_upsert_all_dialects`` (mssql_engine / mssql_async_engine) — the integration ``expected_support`` set still listed ``mssql`` and ``spanner``. The unit-test mirror was updated in the revert but this integration assertion was left behind. Trim it back to the single-row ON-CONFLICT dialects and document the split. 2. ``test_upsert_many_chunk_size_emits_multiple_chunks`` (and its sibling ``test_upsert_many_native_path_is_single_statement_per_chunk``) — the bigint guard introduced by the revert checked ``issubclass(author_model, BigIntBase)``, but the integration fixture uses ``BigIntAuditBase``, which shares only the ``BigIntPrimaryKey`` mixin with ``BigIntBase`` (no subclass relationship). The skip never triggered for ``oracle*``/``mssql``, so the safety-net fallback in ``Repository.upsert_many`` ran and produced one bulk INSERT — failing the ``>= 2 chunks`` assertion. The sibling test happened to pass by coincidence (``insertmanyvalues`` produced exactly the one statement it expected). Switch the guard to ``BigIntPrimaryKey`` so any bigint-PK base catches it.
Post-revert (116b813), the docstring on ``OnConflictUpsert.supports_native_upsert`` was still the generic "Check if the dialect supports native upsert operations." Replace it with the precise scope: the flag is for the single-row ``create_upsert`` API on true ON-CONFLICT dialects; MERGE / INSERT OR UPDATE go through ``Repository.upsert_many`` and aren't reported here. Also extend ``docs/usage/repositories/basics.rst`` with a "Server-managed PK safety net" bullet documenting the resolver's transparent fallback when MERGE / INSERT OR UPDATE rows are missing PK columns — the same behavior the new ``BigIntPrimaryKey`` skip guards in the chunk-size tests exist to side-step.
cofin
force-pushed
the
feat/optimal-upsert-many
branch
from
May 30, 2026 21:22
d80bb6b to
00e3c8a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Repository.upsert_manyandService.upsert_manynow dispatch to the optimal native upsert primitive per dialect whenmatch_fieldsmaps to a PK / UniqueConstraint / unique Index —INSERT … ON CONFLICT DO UPDATE(postgresql/cockroachdb/sqlite/duckdb/mysql/mariadb),MERGE(oracle/mssql), orINSERT OR UPDATE(spanner) — compiled to one statement per chunk.(table, match_fields, dialect)and cached viafunctools.lru_cache; a safety gate ensures non-unique match keys silently fall back to the historical SELECT-then-partition path.chunk_sizekwarg onupsert_manymirroringadd_many.no_merge=Truenow does what it always documented but never delivered — forces the fallback path.Dispatch matrix
INSERT … ON CONFLICT DO UPDATE.returning(model_type)+session.scalarsINSERT … ON DUPLICATE KEY UPDATEmatch_fieldsMERGE INTO … USING (SELECT … FROM DUAL UNION ALL …) …MERGE INTO … USING (VALUES (…)) AS src(…) … OUTPUT inserted.*;INSERT OR UPDATE INTO … (…) VALUES (…)SELECT+add_many+update_many📚 Documentation preview: https://litestar-org.github.io/advanced-alchemy-docs-preview/740