Skip to content

sql: set a cell's column index and kind in one place - #39135

Merged
alii merged 1 commit into
mainfrom
farm/2c9dbbe8/sql-column-identity-helper
Aug 15, 2026
Merged

alii merged 1 commit into
mainfrom
farm/2c9dbbe8/sql-column-identity-helper

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • The mapping from a column's ColumnIdentifier to the two cell fields SQLClient.cpp reads (index, is_indexed_column) was written out four times: mysql/protocol/ResultSet.rs (decode_text and decode_binary), postgres/DataCell.rs (Putter::put_impl), and shared/SQLDataCell.rs (null_for_column).
  • mordant flags the two copies in ResultSet.rs as same_match_twice (the entry in mordant-baseline.toml); a change to one copy would not reach the others.

Fix

  • Adds SQLDataCell::set_column(position, &ColumnIdentifier), which holds the mapping once; the four sites call it (null_for_column is now null() + set_column).
  • Pure refactor: each site sets the same two fields to the same values as before, in the same place in its loop. Net -19 lines.
  • Removes the now-stale same_match_twice:src/sql_jsc/mysql/protocol/ResultSet.rs baseline entry. The CI mordant job is the check that nothing else moved in the baseline.
  • Verified with the debug build against a local MariaDB and Postgres:
    • test/js/sql/sql-mysql-binary-null-indexed.test.ts, sql-mysql-column-name-digits.test.ts, sql-mariadb-json.test.ts (the digit-named column tests cover both MySQL decoders).
    • test/js/sql/sql.test.ts -t "data row that omits columns" (covers null_for_column and put_impl together, with duplicate and digit-named columns).
    • postgres-datarow-overrun, postgres-multi-statement-fields, postgres-binary-array-bounds, sql-mysql-mediumint, sql-mysql-bigint-out-of-range, sql-mysql-datetime-roundtrip: all pass.
    • A probe running the duplicate / digit-named / mixed / NULL-in-indexed / 70-column queries from the suites through objects, .values(), .raw() and .simple() on both drivers produced byte-identical output on this build and on released bun 1.4.0 (the debug build also keeps the isIndexedColumn/isNamedColumn asserts in SQLClient.cpp live).
  • The container-backed matrix in sql-mysql.test.ts / sql.test.ts needs docker, which this environment does not have; CI runs it.

Background

  • SQLDataCell is the #[repr(C)] cell both drivers fill per column and hand to JSC__constructObjectFromDataCell (SQLClient.cpp) to build the row object.
  • ColumnIdentifier is how a result column's name is classified when the column list arrives: Name (an ordinary key), Index(n) (the name is all digits, so the value is stored at array index n, which can be out of order and larger than the column count), or Duplicate (an earlier occurrence of a repeated name, which object results skip; the last occurrence wins).
  • On the cell, is_indexed_column carries that classification as 0 / 1 / 2, and index is the Index number or, otherwise, the column's ordinal position in the row.

The ColumnIdentifier -> (index, is_indexed_column) mapping was written
out four times: both MySQL row decoders, the Postgres Putter, and
SQLDataCell::null_for_column. Move it into SQLDataCell::set_column and
call that from all four sites. No behavior change.

Drops the same_match_twice baseline entry for mysql/protocol/ResultSet.rs.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 14a40486-2966-4f67-9940-22efc59529b9

📥 Commits

Reviewing files that changed from the base of the PR and between 88a6398 and b5bf12e.

📒 Files selected for processing (4)
  • mordant-baseline.toml
  • src/sql_jsc/mysql/protocol/ResultSet.rs
  • src/sql_jsc/postgres/DataCell.rs
  • src/sql_jsc/shared/SQLDataCell.rs

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:05 PM PT - Aug 15th, 2026

@robobun, your commit b5bf12e is building: #98355

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: refactor only, no behavior change intended.

  • Verified locally against MariaDB and Postgres with the debug build: sql-mysql-binary-null-indexed, sql-mysql-column-name-digits, sql-mariadb-json, the postgres short DataRow test in sql.test.ts, and the row-decoding tests listed in the description all pass.
  • A probe running the duplicate / digit-named / mixed column queries from both suites through every result mode gives byte-identical output on this build and on released bun 1.4.0.
  • The docker-backed parts of sql-mysql.test.ts / sql.test.ts and the mordant baseline check are left to CI.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — pure dedup refactor, behavior-preserving at all four sites.

What was reviewed:

  • Traced each of the four call sites (decode_text, decode_binary, put_impl, null_for_column) against the new set_column — same two fields set to the same values; the swapped write order of is_indexed_column/index is unobservable.
  • Confirmed ColumnIdentifier has exactly the three variants the match covers, and Index's payload is u32 (matches cell.index).
  • Checked the now-unconditional u32::try_from(index) in the MySQL loops — index is a bounded column ordinal, and the same conversion already ran on the common Name arm, so no new panic surface.
  • Removed imports (NameOrIndex alias, ColumnIdentifier in DataCell.rs) have no remaining references; the dropped mordant-baseline entry matches the removed duplicate match.
Extended reasoning...

Overview

This PR extracts four byte-identical copies of the ColumnIdentifier → (index, is_indexed_column) mapping into a single SQLDataCell::set_column(position, &ColumnIdentifier) helper. The four sites — MySQL decode_text and decode_binary in ResultSet.rs, Postgres Putter::put_impl in DataCell.rs, and null_for_column in SQLDataCell.rs — now call the helper with the same arguments they previously fed into the inline matches. The stale same_match_twice mordant-baseline entry for ResultSet.rs is removed, and two now-unused imports are dropped. Net -19 lines.

Security risks

None. This is an internal refactor of how two POD fields on a #[repr(C)] cell are populated from an already-classified column identifier. No parsing, no untrusted input handling, no allocation, no FFI signature changes — the C++ side (SQLClient.cpp) sees identical bytes.

Level of scrutiny

Low-to-medium. The touched code is on the per-row hot path for both SQL drivers, but the change is a mechanical extract-method with no control-flow or data-flow changes. I verified line-by-line that each call site produces the same index and is_indexed_column values as before. The one microscopic difference — u32::try_from(loop_index) now runs unconditionally in the MySQL decoders instead of only on the Name/Duplicate arms — cannot introduce a new panic because the loop index is bounded by the column count and the same conversion was already executed for the (far more common) non-Index variants.

Other factors

The ColumnIdentifier enum has exactly three variants (Name, Index(u32), Duplicate), so the helper's matches are exhaustive and equivalent to all four originals. null_for_column's rewrite from a struct literal to null() + set_column is equivalent because null() is SQLDataCell::default(), which the original spread from. The PR description documents thorough verification against local MariaDB/Postgres including the digit-named-column, duplicate-column, and short-DataRow test cases that exercise every branch of this mapping, plus a byte-identical output probe against released 1.4.0. No CODEOWNERS cover these paths and there are no outstanding reviewer comments.

@alii
alii merged commit 371d938 into main Aug 15, 2026
12 of 13 checks passed
@alii
alii deleted the farm/2c9dbbe8/sql-column-identity-helper branch August 15, 2026 17:11
alii added a commit that referenced this pull request Aug 15, 2026
### Problem
- mordant's `reimplemented_helper` finding baselined for
`src/jsc/webcore_types.rs`: `Blob::is_bun_file` and
`Blob::needs_to_read_file` had the same signature and the same body (the
store is `Some` and its data is `Store::File`), so a change to one would
have silently missed the other.
- The duplication is inherited, not a porting mistake: `Blob.zig`
already had `isBunFile` and `needsToReadFile` with the same body, and
neither side has changed since the port (#30412). Nothing relies on them
differing.

### Fix
- Delete `is_bun_file` and keep `needs_to_read_file`, which is what the
~40 other call sites (and the `blob_needs_to_read_file` hook that
`bun_sql_jsc` goes through) already use.
- Point the two `is_bun_file` callers at it: `Response.rs`
(`getCompleteWebRequestOrResponseBodyValueAsArrayBuffer` returns
`undefined` for a file-backed body) and `CryptoHasher.rs` (the
synchronous hashers reject `Bun.file()` input). Same predicate, so no
behavior change.
- Remove the `reimplemented_helper:src/jsc/webcore_types.rs` entry from
`mordant-baseline.toml`, and the two stale comments in `webcore/Blob.rs`
that named the deleted method. The entry is removed by hand rather than
by regenerating the file: a full `bun run rust:mordant:baseline` on
Linux also drops two unrelated entries (`always_unwrapped_option` in
`PackageInstall.rs`, `narrowed_two_ways` in `node_crypto_binding.rs`)
that this PR has no business touching.
- No new test: there is no observable behavior to pin (the two
predicates were byte-for-byte the same), so no test can distinguish
before from after. The `Bun.file()` rejection at the `CryptoHasher` call
site is already covered by `bun-cryptohasher.test.ts` ("Bun.file in
CryptoHasher is not supported yet"), and the mordant run below is the
check for the finding itself. Same shape as #39124, #39116 and #39135
from this batch.
- Verified:
- `bun run rust:mordant` (full workspace) on this branch: clean, no
`target/mordant/over-baseline.txt`. With main's `webcore_types.rs`
restored on top of the trimmed baseline it reports `reimplemented_helper
... over the mordant baseline (0 recorded for src/jsc/webcore_types.rs)`
and `1 finding(s) over the baseline in bun_jsc`, so the entry was this
site and the lint no longer fires.
- The `mordant` workflow also runs on this PR since the baseline
changed; with the entry removed it fails if the finding is still
reported.
- `bun bd test test/js/bun/util/bun-cryptohasher.test.ts` (covers the
`CryptoHasher` call site: `Bun.file()` input still throws): 402 pass.
- `bun bd test` on
`test/js/web/fetch/{blob,blob-write,body,blob-file-name-ownership,response}.test.ts`,
`test/js/web/structured-clone-blob-file.test.ts` and
`test/js/bun/io/bun-write.test.js` (`--timeout 60000`, since several of
these spawn a debug+ASAN child and exceed the 5s default): everything
passes except `blob.test.ts` "Bun.file(path).slice(start, end) streams
only the slice", which fails identically on a clean checkout of main
(88a6398) and is being handled separately; it is a `FileReader`
streaming bug unrelated to this predicate.
- Two open PRs add `is_bun_file()` calls in `Blob.rs` (#32434, #33659);
whichever lands after this one needs those calls renamed to
`needs_to_read_file()`. That is a compile error, not a silent change.

### Background
- `Blob` (`src/jsc/webcore_types.rs`) is a view (offset + size) onto a
refcounted `Store`, whose `data` is one of `Bytes` (in memory), `File`
(a path or fd; what `Bun.file()` creates) or `S3`.
`needs_to_read_file()` asks whether the blob is `File`-backed, i.e.
whether its bytes have to be read off disk before anything in memory can
look at them; synchronous consumers use it to bail out.
- mordant is the advisory Rust lint pack the `rust-lints` workflow runs
(`bun run rust:mordant`). `mordant-baseline.toml` holds the per-(lint,
file) counts of findings that predate the job, so CI only fails when a
count goes up; once a baselined finding is fixed its entry is deleted.

Co-authored-by: Alistair Smith <hi@alistair.sh>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants