Skip to content

service-package's local normalizeRows never unwraps the mysql2 [rows, fields] tuple — a populated result reads as "not installed" #11062

Description

@os-warren

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:

functionnormalizeRows(result: any): any[]{if(Array.isArray(result))returnresult;if(result&&Array.isArray(result.rows))returnresult.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])){returnresult[0]asRecord<string,unknown>[];}returnresultasRecord<string,unknown>[];}
#shapedialectlocal copy
1bare row arraybetter-sqlite3 through knex, Tursoflattened
2{ rows, rowCount, … }pgflattened
3[rows, fields] tuplemysql2not 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

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions