fix(flow): treat an empty tenant as unattributed, not as a tenant - #469
Merged
Conversation
Cloud review on PR #468 found the flow_run_nodes backfill bucketing on `(string) $run->tenant_id` with no validation, while the guard behind it checked only whereNull. A run whose tenant_id is '' therefore stamped every one of its nodes '' and the migration reported success. That is strictly worse than the outcome this migration was written to prevent. Its whole argument for NULL over 'default' is that NULL is "distinguishable, invisible to `where tenant_id = ?`, and repairable". An empty tenant is invisible to the query AND invisible to the guard — undetectable rather than merely wrong, and the one value that passes every gate while leaving rows no tenant can read. It is not hypothetical either: TenantScopedDashboardReads::apply() guards `$tenantId === ''` explicitly with `whereRaw('1 = 0')`, which is the codebase already conceding the state occurs. Two changes, because one alone leaves half the hole open. The backfill now trims and skips an empty tenant, so those nodes stay NULL instead of being stamped; the guard now counts empty alongside NULL, so anything that reaches it either way is refused by name. Whitespace is trimmed for the same reason the empty case exists — " " is non-empty to a `= ''` comparison and still matches no real tenant. The runbook carried the identical blind spot in its pre-flight gate (`count(*) FILTER (WHERE tenant_id IS NULL)`), so an operator would have cleared that check and hit the silent mis-stamp anyway. Fixed there too. Mutation-checked in both directions: reverting the guard alone leaves the whitespace case failing, reverting both leaves both new cases failing, and the fix turns both green. The existing orphan-node test moves to the new message text.
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The migration now fails closed on empty/blank tenants, and the behavior is covered by targeted feature tests plus aligned operational documentation.
Pull request overview
This PR fixes a data-integrity hole in the flow_run_nodes tenant backfill migration by treating empty/blank tenant_id values as “unattributed” (i.e., invalid) instead of letting them silently propagate into persisted nodes, and updates the operational runbook and tests to match.
Changes:
- Migration backfill now trims
flow_runs.tenant_idand skips empty results, leaving affected nodesNULLso the migration fails closed. - Tightening guard now refuses both
NULLand empty-stringtenant_idvalues and updates the exception message to reflect both root causes. - Runbook preflight SQL and migration feature tests are updated to catch empty/whitespace tenant defects and assert on the new error wording.
File summaries
| File | Description |
|---|---|
| database/migrations/2026_10_02_000008_add_tenant_id_to_flow_run_nodes.php | Trim-and-skip empty run tenants during backfill; guard counts NULL and ''; improved failure message. |
| tests/Feature/Migrations/AddTenantIdToFlowRunNodesTest.php | Adds explicit coverage for '' and whitespace-only run tenants and updates expected error message text. |
| docs/runbooks/flow-v2-migration.md | Updates preflight query to include empty/blank tenants and documents why this arm is required. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Found by cloud review on #468. The finding is about code already on main, so it gets its own PR rather than being smuggled into a sync commit — patching it inside #468 would make develop diverge from main on the very files that just conflicted.
The defect
backfillFromRuns()bucketed on(string) $run->tenant_idwith no validation, andtightenToHostShape()behind it checked onlywhereNull. So a run whosetenant_idis''stamped every one of its nodes'', and the migration reported success.That is strictly worse than the outcome this migration exists to prevent. Its whole argument for NULL over
defaultis that NULL is "distinguishable, invisible towhere tenant_id = ?, and repairable". An empty tenant is invisible to the query and invisible to the guard — undetectable rather than merely wrong, and the one value that passes every gate while leaving rows no tenant can read.Not hypothetical:
TenantScopedDashboardReads::apply()guards$tenantId === ''explicitly withwhereRaw('1 = 0'). The codebase already concedes the state occurs.The fix, in two halves
Either half alone leaves the hole open:
Whitespace is trimmed for the same reason the empty case exists —
" "is non-empty to a= ''comparison and still matches no real tenant.The runbook had the same blind spot
This is the part I would not have caught.
docs/runbooks/flow-v2-migration.mdpre-flight gate 1b read:An operator would have cleared that check and hit the silent mis-stamp regardless. Now
WHERE tenant_id IS NULL OR btrim(tenant_id) = '', with the reasoning stated so the empty arm does not look like defensive padding to whoever trims it later.Verification
Mutation-checked in both directions, because a guard test that cannot fail is the failure mode this whole area keeps producing:
The existing orphan-node test moves to the new message text (
have no usable tenant after the backfill) — worth noting it was asserting on the old wording, so leaving the message unchanged would have been the easy way to keep it passing while saying something less accurate.R40 local critic:
0 must-fix, 0 nit.🤖 Generated with Claude Code
https://claude.ai/code/session_019eS9Cj8rvPK85N517Emn9h