Skip to content

Match bulkCreate upsert results to instances by conflict key - #164

Draft
rafiss wants to merge 1 commit into
masterfrom
rafiss/adoring-greider-45bb7e
Draft

rafiss wants to merge 1 commit into
masterfrom
rafiss/adoring-greider-45bb7e

Conversation

@rafiss

@rafiss rafiss commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Model.bulkCreate assigns the rows returned by
INSERT ... ON CONFLICT DO UPDATE ... RETURNING to its instances by
position, which assumes that the rows come back in the order of the
VALUES list. CockroachDB does not guarantee this: it plans the conflict
check as a left outer join of the VALUES against the conflict index,
and both the lookup join and the hash join can return rows that matched
an existing row (and are updated) before rows that did not (and are
inserted). With updateOnDuplicate, instances could be populated with
the primary key and values of a different row. This happens for any
conflict target, including the primary key, not only for partial
indexes with conflictWhere.

Wrap bulkInsert so that, when updateOnDuplicate is used, the returned
rows are matched back to the input records by the values of the
conflict target columns (upsertKeys). Records with a NULL or DEFAULT
conflict key are always inserted, so they are matched to the remaining
rows in order. If the rows cannot be matched unambiguously, they are
returned unchanged.

Plain INSERT, UPSERT and ON CONFLICT DO NOTHING without conflicts
return rows in input order today, so bulkCreate without
updateOnDuplicate is not changed.

Stop ignoring the Sequelize "should upsert existing memberships" test,
which now passes.

🤖 Generated with Claude Code

Model.bulkCreate assigns the rows returned by
INSERT ... ON CONFLICT DO UPDATE ... RETURNING to its instances by
position, which assumes that the rows come back in the order of the
VALUES list. CockroachDB does not guarantee this: it plans the conflict
check as a left outer join of the VALUES against the conflict index,
and both the lookup join and the hash join can return rows that matched
an existing row (and are updated) before rows that did not (and are
inserted). With updateOnDuplicate, instances could be populated with
the primary key and values of a different row. This happens for any
conflict target, including the primary key, not only for partial
indexes with conflictWhere.

Wrap bulkInsert so that, when updateOnDuplicate is used, the returned
rows are matched back to the input records by the values of the
conflict target columns (upsertKeys). Records with a NULL or DEFAULT
conflict key are always inserted, so they are matched to the remaining
rows in order. If the rows cannot be matched unambiguously, they are
returned unchanged.

Plain INSERT, UPSERT and ON CONFLICT DO NOTHING without conflicts
return rows in input order today, so bulkCreate without
updateOnDuplicate is not changed.

Stop ignoring the Sequelize "should upsert existing memberships" test,
which now passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant