RocketBoost Orbit logo Orbit rocketboost.rs

Differences from Laravel

Orbit follows Laravel's Eloquent and query builder, with these exceptions. Each one is listed here so it is not a surprise to someone who knows Laravel.

  • first() has no default order. As in Laravel, first() takes the first row the database returns. Call order_by when the row matters. Laravel's sole, chunk and lazy enforce an order; Orbit does not add one.
  • Columns outside FILLABLE are an error. insert and update return Error::NotFillable for a column that is not in the model's FILLABLE list. Laravel's Model::create drops such an attribute without a message, so a misspelt column is ignored. Orbit stops instead, so the mistake shows up at once. Laravel's query builder does not check $fillable at all; Orbit checks it on every write.
  • insert returns the id. It returns the auto-increment id as u64. Laravel's query builder insert returns a boolean, and insertGetId returns the id.
  • where_eq(column, None) is IS NULL, and Operator::Ne with None is IS NOT NULL, as in Laravel. Other operators with None match nothing, where Laravel throws.
  • Terminal methods take the database. get(&db) and the other terminal methods take the Database explicitly. Laravel uses a default connection.
  • Transactions take an async closure that receives the transaction. Database::transaction passes the transaction as tx, and queries run on it when given &mut *tx. Laravel's DB::transaction takes no argument and uses the default connection. The closure's error type must convert from Error.
  • Transactions do not retry. Laravel's DB::transaction($callback, $attempts) retries on a deadlock. Orbit returns Error::Deadlock and leaves the retry to the caller.
  • Grouped conditions are where_group and or_where_group. They take a closure that receives an empty query, as Laravel's where(fn) does. Only the conditions are kept from the group; order_by, limit and the other settings inside it are ignored.
  • Errors are typed. Unique and foreign key violations, deadlocks and lock wait timeouts are variants of Error. Laravel's QueryException carries the SQLSTATE code instead. A unique violation's index name comes from the server's message, because the MySQL driver does not return it as a separate field.
  • replace is Diesel's replace_into, not Laravel's upsert. Query::replace issues MySQL's REPLACE INTO, with Diesel's semantics. It deletes the row that has the same unique key or primary key, then inserts the new one. So the auto-increment id changes, columns not in the values take their defaults, and the returned count is the rows affected as MySQL reports them (2 for a replaced row). Laravel's upsert is INSERT ... ON DUPLICATE KEY UPDATE, which updates the existing row in place and keeps its id. That is a different operation, and Orbit does not implement it.
  • The blocking API refuses async threads. orbit_orm::blocking (feature blocking) runs the async queries on a runtime that the Database owns. Laravel has no sync or async split, so this follows Diesel, which Ignite uses. A blocking call on a thread that runs async code returns Error::BlockingInRuntime, rather than panicking inside Tokio or stalling the runtime. Call it from tokio::task::spawn_blocking, as Ignite does. Each call blocks its thread, so the blocking pool is the place for it. The Database shuts its runtime down in the background when dropped, so dropping it on an async thread is safe.

Matches Laravel, with limits

  • for_update runs outside a transaction. Outside a transaction, for_update runs SELECT ... FOR UPDATE in autocommit, and the lock lasts for that statement. This matches Laravel's lockForUpdate().
  • The default table name follows Laravel's inflector, with gaps. Without table, the name is Str::snake(Str::pluralStudly(name)), as in Eloquent: only the last word is pluralized, so UserProfile gives user_profiles and Person gives people. The plural rules are ported from doctrine/inflector 2.1.x, the English rules Laravel's Pluralizer uses, with the same uncountable words, irregular words and suffix rules in the same order. Checked against Laravel v13.42.0 (illuminate/support 13.42.0), which requires doctrine/inflector ^2.0.5 and resolves to 2.1.1: 1,693 distinct class names (1,862 rows over two lists) give the same Str::snake(Str::pluralStudly(name)) as Orbit. Before the fixes from that check, 13 of them differed (Media, People, Trivia, Maltese and Testis, alone and in compounds); those are fixed. Gaps: the rules are ASCII only, so a name that ends in a non-ASCII letter gets a plain s; the check covered doctrine/inflector 2.1.1 only, and names outside the tested set are not proven to match every doctrine rule; a word that matches no rule gets an s, as Laravel does. table = "..." overrides the default, and an explicit name is used as written.