[13.x] Keep Eloquent on the direct connection during migrations - #61092
[13.x] Keep Eloquent on the direct connection during migrations#61092RobertoNegro wants to merge 1 commit into
Conversation
37ded63 to
f0aa468
Compare
Builder::newModelInstance() re-pinned every model it built with Connection::getName(), which returns the base config name and so discarded the ::direct routing. Model writes inside a migration therefore ran on the pooled connection, outside the migration's transaction. Adds Connection::getNameWithDirectType(), which keeps the suffix for the direct variant and collapses read/write to the base name as before, and uses it everywhere a model is bound to its connection's name: Builder::newModelInstance(), Factory::store(), the fresh model fallback in Model::save(), and MorphTo::createModelByType().
f0aa468 to
806b9be
Compare
This is PostgreSQL-specific naming, this shouldn't bleed into Laravel core functionality. |
|
|
|
Then nevermind— |
It refers to the configured direct endpoint: a connection that reaches the database directly and bypasses the transaction pooler. I do not think ::readwrite would describe it accurately, the regular connection already supports both reads and writes, and ::read / ::write describe PDO routing. ::direct instead distinguishes the target endpoint and transaction context: pooled vs direct. The direct configuration and ::direct routing name were introduced in 13.x by #60425. This PR only fixes Eloquent dropping that existing routing name while binding models. Renaming or revisiting the routing terminology would be a separate API and backward-compatibility discussion. |
|
Ah, thanks for the explanation 👍🏻 |
The transaction-pooler support added in #60425 routes migrations to the
::directconnection, andMigrator::runMethod()sets the default connection accordingly:Builder::newModelInstance()then throws that away:getName()returnsgetConfig('name'), andnewModelInstance()is the funnel for most models produced by a builder,hydrate()included. So a model created in a migration is written to the pooled connection, outside the migration's transaction, and a model read on the direct connection comes back bound to"pgsql", taking any latersave()ordelete()with it. Statements that never build a model (massupdate(),delete(),upsert()) stay on the direct connection.On a fresh database this fails outright (
relation "..." does not exist, since the table was created inside the direct connection's transaction). On an existing one it succeeds silently and is no longer rolled back with the DDL.Dropping the suffix is right for
::read/::write, where a model read from a replica must stay writable.::directserves both reads and writes, so there it changes which server the statement reaches.This adds
Connection::getNameWithDirectType(), which keeps the suffix only for the direct variant, and uses it wherever Eloquent binds a model to a resolved connection name:Builder::newModelInstance(),Factory::store()(reachable throughmigrate --seed, whichMigrateCommandruns insideusingConnection()), the new-model fallback inModel::save(), andMorphTo::createModelByType(). The latter two cover models created outside an Eloquent builder, such as relation-created and polymorphic models.Reproduction
With a
directendpoint configured, run a migration that creates a table and then writes to it through Eloquent. Either form lands on the pooled connection:migrate:freshfails withrelation "examples" does not exist. A pooler is not needed to see it: pointingdirectat the same host as the pooled connection is enough, since the two are separateConnectioninstances with separate transactions.Backward compatibility
getNameWithDirectType()returns the same value asgetName()unless the connection is thedirectvariant. Read/write routing is untouched and covered by a test, andgetName()andgetNameWithReadWriteType()are unchanged.Tests
Unit tests cover both halves of the new method: that it keeps the
::directsuffix for the direct variant, and thatnull,readandwritestill collapse to the base name, so read/write routing cannot regress. Two more assert thatnewModelInstance()binds a model to whichever of the two the connection reports.The regression itself is covered by an integration test, added to the file #60425 introduced. It creates a table inside a transaction on the direct connection, then writes to it and reads it back through a model, so anything falling back to the pooled connection cannot see it. That test case already points
directat the same host as the pooled connection, so CI needs no extra service. Without the patch it fails withrelation "pooled_direct_models" does not exist (Connection: pgsql, ...).Found while moving a production application onto this configuration, and verified there against PostgreSQL 18 behind PgBouncer 1.25.2 in
pool_mode = transaction.