Skip to content

feat(orm): Eloquent-style upsert() on the async Masonite ORM#129

Closed
bedus-creation wants to merge 1 commit into
mainfrom
task/orm-upsert
Closed

feat(orm): Eloquent-style upsert() on the async Masonite ORM#129
bedus-creation wants to merge 1 commit into
mainfrom
task/orm-upsert

Conversation

@bedus-creation

Copy link
Copy Markdown
Contributor

Summary

Adds a Laravel/Eloquent-style upsert() to the async Masonite ORM, exposed on both the Model (classmethod) and the AsyncQueryBuilder, mirroring how create() / bulk insert() are exposed today.

await Flight.upsert(
    [{"number": "AA1", "seats": 100}, {"number": "AA2", "seats": 80}],
    unique_by=["number"],
    update=["seats"],
)
  • Inserts all rows in a single bulk statement (not a loop).
  • On collision over the unique_by columns, UPDATEs only the columns in update.
  • When update is omitted, all non-unique columns are updated (Laravel behaviour).
  • Accepts a single dict or a list of dicts; unique_by / update accept a string or list.
  • Returns the affected row count; returns 0 for empty input.

Dialect-specific SQL

Built with SQLAlchemy's dialect insert helpers so each driver gets the correct clause:

Driver Generated clause
Postgres INSERT ... ON CONFLICT (unique_by) DO UPDATE SET col = excluded.col
SQLite INSERT ... ON CONFLICT (unique_by) DO UPDATE SET col = excluded.col
MySQL INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col)

When the resolved update set is empty, it degrades to DO NOTHING (Postgres/SQLite) or a no-op key update (MySQL).

Implementation

  • masoniteorm/query/upsert.pybuild_upsert_statement() picks the dialect insert helper from the connection driver and builds one bulk statement over a lightweight sqlalchemy.table(...).
  • QueryBuilder.upsert() — normalises rows to a shared, deterministic column set and runs values through the model's casters (consistent with existing insert paths), then executes the construct.
  • Connection.execute_statement() — executes a SQLAlchemy core construct directly (commits when not inside a transaction), since upsert SQL is built with dialect helpers rather than the grammar.
  • Model.upsert() — classmethod delegating to the builder.

Tests

  • tests/masoniteorm/sqlite/builder/test_sqlite_builder_upsert.py — end-to-end against SQLite: insert-only, single dict, update-on-conflict without duplicating, update-list restriction, default (all non-unique) update, mixed insert+update in one call, empty input.
  • tests/masoniteorm/test_upsert_statement.py — compiles the statement for Postgres, SQLite and MySQL and asserts the exact dialect clause + single bulk INSERT.

Full ORM suite (excluding live Postgres/MySQL): 386 passed, 7 skipped. New tests: 13 passed.

Add upsert() on both Model and AsyncQueryBuilder. Rows are inserted in a
single bulk statement; on a unique_by collision the listed columns are
updated (all non-unique columns when update is omitted).

SQL is dialect-specific via SQLAlchemy dialect insert helpers:
- Postgres/SQLite: INSERT ... ON CONFLICT (unique_by) DO UPDATE SET col = excluded.col
- MySQL: INSERT ... ON DUPLICATE KEY UPDATE col = VALUES(col)

Values are normalised to a shared column set and run through the model's
casters, consistent with existing insert paths.
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