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:

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions