Skip to content

fix: prevent Orm.Connection() cached path from poisoning dbConfig - #1540

Merged
hwbrzzl merged 3 commits into
masterfrom
fix/orm-connection-poisoned-dbconfig
Sep 1, 2026
Merged

fix: prevent Orm.Connection() cached path from poisoning dbConfig#1540
hwbrzzl merged 3 commits into
masterfrom
fix/orm-connection-poisoned-dbconfig

Conversation

@goravel-coder

@goravel-coder goravel-coder commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Orm.Connection() now resolves each connection's own database config from a shared, concurrency-safe QueriesCache, so Name()/DatabaseName() and connection-scoped behavior (like where the migration ledger row is written) reflect the requested connection instead of the default's.
  • Concurrent Connection() calls that lazily build the same connection are now serialized by double-checked locking, eliminating the data race on the connection cache — TestOrmConnectionConcurrentSafe passes under -race and fails on the unlocked version.
  • The exported database/orm/NewOrm constructor now takes a *QueriesCache built via the new NewQueriesCache(queries, dbConfigs) — breaking only for direct callers of the concrete constructor; contractsorm.Orm is unchanged, so no mock regeneration is needed.

goravel/goravel#987

Why

Orm.Connection() had a cached fast path that ignored the requested connection and reused the receiver's dbConfig. Once a non-default connection had been resolved, every later Connection() call for it returned an Orm whose Name()/DatabaseName() — and therefore derived behavior like where the migration ledger row gets written — still pointed at the default connection. Running two migrations on the same non-default Connection() wrote the second ledger row to the wrong database.

// app/database/migrations — two consecutive migrations on the same non-default connection
func (r *M20260826160940CreateUsersTable) Connection() string {
	return "sqlite"
}

func (r *M20260826161140CreateUserTokensTable) Connection() string {
	return "sqlite"
}

Before the fix, the second migration's ledger row was written to the sqlite connection, where no migrations table exists, failing the run. After the fix, both migrations run on their declared sqlite connection while both ledger rows are recorded on the default connection, matching Laravel's behavior.

The same fix hardens the connection cache for concurrent use. Lazy connection builds now flow through a shared QueriesCache guarded by an RWMutex with double-checked locking, so concurrent cold-path calls from different goroutines no longer race on the shared maps; Observe() snapshots the cache under a read lock before invoking user code. As a cleanup, the ambiguous internal cache field was renamed from m to connections.

// Concurrent resolution of the same lazily-built connection from multiple goroutines
var wg sync.WaitGroup
for range 10 {
	wg.Add(1)
	go func() {
		defer wg.Done()
		orm := facades.Orm().Connection("sqlite")
		// ...
	}()
}
wg.Wait()

Before the fix, these concurrent cold-path calls raced on the shared connection maps (a DATA RACE under -race). After the fix, double-checked locking serializes the build and every caller gets an Orm bound to the requested connection.

Fixes goravel issue #987. Cache each connection's database.Config alongside
its Query so Orm.Connection(name) returns an Orm whose Config/DatabaseName/
Name match the requested connection, and Name() reports the connection it
was constructed for. This stops consecutive migrations on a non-default
connection from writing the second ledger row to the wrong database.
@goravel-coder
goravel-coder requested a review from a team as a code owner August 30, 2026 00:57
@codecov

codecov Bot commented Aug 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.37%. Comparing base (fda6f87) to head (875f806).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
database/orm/orm.go 0.00% 23 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1540      +/-   ##
==========================================
- Coverage   72.41%   72.37%   -0.04%     
==========================================
  Files         409      409              
  Lines       26475    26489      +14     
==========================================
  Hits        19172    19172              
- Misses       7301     7315      +14     
  Partials        2        2              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…-path races

Wrap the shared connection cache in a QueriesCache guarded by an RWMutex and
use double-checked locking in Connection(), so concurrent lazy builds of the
same connection from different goroutines are serialized instead of racing on
the maps. Observe() snapshots the cache under RLock before invoking user code.
Adds TestOrmConnectionConcurrentSafe, which passes under -race and fails on
the unlocked version.
@hwbrzzl
hwbrzzl merged commit 8410a2d into master Sep 1, 2026
19 of 21 checks passed
@hwbrzzl
hwbrzzl deleted the fix/orm-connection-poisoned-dbconfig branch September 1, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants