Found while fixing #10965 (PR link below), by enumerating the dialect shapes from the code rather than from that card's summary. Not fixed there — different defect class, different blast radius.
The defect
#10965 describes packages/services/service-package/src/index.ts as carrying "the same three-dialect flattener metadata-protocol exports". Read against the source, it does not. The local copy has exactly two accepting branches:
function normalizeRows(result: any): any[] {
if (Array.isArray(result)) return result;
if (result && Array.isArray(result.rows)) return result.rows;
return [];
}
metadata-protocol's copy (src/migrations/seed-tenancy-backfill.ts:362) has three — it unwraps the mysql2 tuple first:
if (Array.isArray(result)) {
// mysql2's `[rows, fields]`: the first element is itself the row array.
if (result.length > 0 && Array.isArray(result[0])) {
return result[0] as Record<string, unknown>[];
}
return result as Record<string, unknown>[];
}
| # |
shape |
dialect |
local copy |
| 1 |
bare row array |
better-sqlite3 through knex, Turso |
flattened |
| 2 |
{ rows, rowCount, … } |
pg |
flattened |
| 3 |
[rows, fields] tuple |
mysql2 |
not unwrapped |
The doc comment above the local copy names three bullets, but bullets 2 and 3 are both { rows } — so the prose reads as three-dialect coverage while the code implements two. That is likely how the gap survived.
Why it matters — traced by reading, NOT reproduced
On a mysql2-backed seam, normalizeRows([[row], [fields]]) returns the whole 2-element tuple rather than the row array. get() then reads rows[0], which is the row array, so row.manifest is undefined and JSON.parse(undefined) throws — into get()'s own catch, which returns null. list() maps over the tuple and throws in the same place, into its catch, which returns [].
So the caller-visible answers are, again, "this package is not installed" and "no packages are installed" — over a driver that answered correctly with real rows.
⚠️ This is traced from the source, not reproduced: no mysql2 driver was booted, and none is loaded by any suite that exercises this file. Someone should reproduce before sizing it. It is also worth checking whether a mysql2 seam reaches this service in any supported composition at all — if it cannot, this is latent rather than live.
Why this is a different card from #10965
#10965 is a seam that could not answer being read as empty. This is a seam that answered being misread — the query ran, the rows came back, and the flattener dropped them. Different class, and the fix is a different line. #10965's fix does not touch it: its isResultSet guard correctly treats a tuple as an answer (it is an array), so no false refusal is introduced, and PR #REPLACE_PR pins exactly that non-misfire. It simply does not unwrap the tuple, because that was never in its scope.
Not measured here
Refs
#10965 · #10677 / PR #10788 · #10789 / PR #10964
Found while fixing #10965 (PR link below), by enumerating the dialect shapes from the code rather than from that card's summary. Not fixed there — different defect class, different blast radius.
The defect
#10965 describes
packages/services/service-package/src/index.tsas carrying "the same three-dialect flattenermetadata-protocolexports". Read against the source, it does not. The local copy has exactly two accepting branches:metadata-protocol's copy (src/migrations/seed-tenancy-backfill.ts:362) has three — it unwraps the mysql2 tuple first:{ rows, rowCount, … }[rows, fields]tupleThe doc comment above the local copy names three bullets, but bullets 2 and 3 are both
{ rows }— so the prose reads as three-dialect coverage while the code implements two. That is likely how the gap survived.Why it matters — traced by reading, NOT reproduced
On a mysql2-backed seam,
normalizeRows([[row], [fields]])returns the whole 2-element tuple rather than the row array.get()then readsrows[0], which is the row array, sorow.manifestisundefinedandJSON.parse(undefined)throws — intoget()'s own catch, which returnsnull.list()maps over the tuple and throws in the same place, into its catch, which returns[].So the caller-visible answers are, again, "this package is not installed" and "no packages are installed" — over a driver that answered correctly with real rows.
Why this is a different card from #10965
#10965 is a seam that could not answer being read as empty. This is a seam that answered being misread — the query ran, the rows came back, and the flattener dropped them. Different class, and the fix is a different line. #10965's fix does not touch it: its
isResultSetguard correctly treats a tuple as an answer (it is an array), so no false refusal is introduced, and PR #REPLACE_PR pins exactly that non-misfire. It simply does not unwrap the tuple, because that was never in its scope.Not measured here
objectql.executefor this service.packages/cli/src/commands/migrate/duplicates.ts:521— anisResultSet, not a flattener) has a matching gap in whatever it uses to flatten.metadata-protocol's branch into this local flattener or to unify the copies — the latter being the open question service-package answers "no such package" / "no packages installed" over a driver it never queried — its own normalizeRows maps a non-answering seam onto zero rows #10965 already raises forisResultSet, and this finding is a second data point for it.Refs
#10965 · #10677 / PR #10788 · #10789 / PR #10964