Skip to content

Remove TryInsertResult::Empty#3103

Open
Huliiiiii wants to merge 1 commit into
SeaQL:masterfrom
Huliiiiii:try-insert-empty
Open

Remove TryInsertResult::Empty#3103
Huliiiiii wants to merge 1 commit into
SeaQL:masterfrom
Huliiiiii:try-insert-empty

Conversation

@Huliiiiii

Copy link
Copy Markdown
Member

This fixes a legacy semantic issue. In practice, the Empty branch is usually unreachable because empty inputs are already handled by InsertMany, so removing it makes the semantics clearer.

At this point, TryInsertResult is effectively equivalent to Option, so it could be further simplified to use Option instead of a custom type. Users can still reproduce the previous TryInsert::Conflict semantics with their own input.is_empty() and last_insert_id.is_none() checks when needed.

The changelog needs to be updated to explain the corresponding behavioral changes.

Breaking Changes

  • Remove TryInsertResult::Empty

@Huliiiiii
Huliiiiii requested review from Expurple and tyt2y3 June 30, 2026 17:11

@tyt2y3 tyt2y3 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The concern

To delete Empty, the PR severs the InsertMany -> TryInsert bridge, so batch inserts no longer produce a TryInsert. But on_conflict_do_nothing on a batch still needs a way to say "ran, nothing inserted." The only remaining return type is InsertManyResult { last_insert_id: Option<_> }, which has no conflicted state — so the PR maps it away:

Err(DbErr::RecordNotInserted) => Ok(InsertManyResult { last_insert_id: None }),

Two problems with that:

  1. Conflicted collapses into None, indistinguishable from empty input. For a batch, "empty input (no SQL run)", "all rows conflicted (SQL ran, 0 inserted)", and "inserted (got a last id)" are three genuinely distinct outcomes. Folding them into Option<id> silently loses the middle one. The "effectively equivalent to Option" observation only holds after Conflicted has already been discarded — it's a bit circular.

  2. The swallow is on InsertMany::exec itself, not gated behind on_conflict_do_nothing. So every batch insert now turns RecordNotInserted into Ok(None), including a plain insert_many(models).exec() that previously returned Err(RecordNotInserted). That masks errors broadly — and it's the path we use internally to insert M2M junction rows, so an after-save that silently inserts nothing would no longer surface.

Why the rationale only covers half

The justification — empty input is already handled by InsertMany::exec returning None, so Empty is redundant — is fair, but it only speaks to the empty case (and only on the single-row path, where empty can't occur anyway). It says nothing about Conflicted. Effectively two changes are bundled here: (a) drop Empty, which is defensible, and (b) drop the batch's Conflicted signal, which isn't justified.

@tyt2y3

tyt2y3 commented Jul 19, 2026

Copy link
Copy Markdown
Member

I'd rather preserve explicit Conflicted

@Huliiiiii

Copy link
Copy Markdown
Member Author

Why don't we just return DbErr::RecordNotInserted?

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.

2 participants