Skip to content

sql: expose column type metadata on query results - #30037

Open
robobun wants to merge 3 commits into
mainfrom
farm/2d0df50e/sql-result-columns
Open

sql: expose column type metadata on query results#30037
robobun wants to merge 3 commits into
mainfrom
farm/2d0df50e/sql-result-columns

Conversation

@robobun

@robobun robobun commented May 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes #26809. Fixes #18866. Supersedes #18892.

Problem

Bun.sql auto-parses query results (JSONB → JS objects, arrays → JS arrays) but doesn't expose the underlying PostgreSQL column type OIDs. This makes it impossible to distinguish between:

  • A jsonb column containing ["a","b"] (should be treated as JSON)
  • A text[] column containing ["a","b"] (should be treated as a text array)

Both produce identical JS arrays. Driver adapters (e.g. Prisma) need the wire-protocol column types to handle these correctly.

What this adds

Query results now carry .columns and .statement:

const result = await sql`select '["a","b"]'::jsonb as data, ARRAY['a','b']::text[] as tags`;

result.columns
// [
//   { name: "data", type: 3802, table: 0, number: 0 },   // jsonb
//   { name: "tags", type: 1009, table: 0, number: 0 },   // text[]
// ]

result.statement
// { string: "select ...", columns: [...] }

Works for tagged templates, .unsafe(), .values(), .raw(), and .simple() (including multi-statement queries, where each result set gets its own columns).

PostgreSQL{ name, type, table, number } (type = OID from pg_type, matches postgres.js's result.columns).
MySQL/MariaDB{ name, type, table, length, flags } (type = protocol column-type code).
SQLitenull (no wire-protocol row description).

Implementation

Rather than a getter on the native query handle (PR #18892's approach, which has lifetime issues when a simple query's RowDescription is overwritten between result sets), the column metadata is built in onResult / resolve and passed as an extra argument to the JS resolve callback, which stores it directly on the SQLResultArray. This makes multi-statement .simple() queries return correct per-result-set columns.

FieldDescription now stores the raw column name alongside name_or_index, so duplicate column names survive checkForDuplicateFields() and are reported correctly in .columns (matching postgres.js).

The { string, columns } object is built once per statement and held on the native statement via a Strong reference (same lifetime/invalidation policy as cached_structure), so re-executing a prepared statement reuses it instead of rebuilding per query — matching postgres.js, where statement is the shared prepared-statement descriptor. It is invalidated wherever the statement's fields are cleared or replaced (simple-protocol batches, result-set column-count changes). On MySQL, where the server re-sends column definitions on every execution, ColumnDefinition41::decode reports what a re-decode changed as Changed { structure, metadata }: structure (the pre-existing name_or_index signal) invalidates the row structure and duplicate check, while metadata (raw name, table, type, length, flags) invalidates the statement object, so equal-width result sets that differ only in type get fresh metadata without rebuilding the row structure. Together with skipping the ColumnDefinition41 name/table re-copy when the re-decoded bytes are unchanged, repeated executions stay allocation-flat: without this, the per-query churn pushed test/regression/issue/28632.test.ts (MySQL RSS regression test) from ~7 MB to ~44 MB growth on the x64-asan lane (threshold 36 MB); with it, a local run of the same 5000-query loop against MariaDB shows no measurable growth over the no-feature baseline.

Verification

  • test/js/sql/postgres-result-columns.test.ts — mock Postgres wire-protocol server (no database required): name/type/table/number incl. negative attnums, zero-row result sets, duplicate names, per-result-set columns for multi-statement .simple(), and no stale-column inheritance for commands without a RowDescription. Fails without the native changes, passes with them.
  • test/js/sql/sql.test.ts / test/js/sql/sql-mysql.test.ts (docker-gated) — jsonb vs text[] OIDs, table OIDs, .unsafe()/.values()/.raw()/.simple(), long (>15 byte) column-name ownership, and result.statement / result.columns object reuse across re-executions of the same prepared statement.

Rebase notes

  • test(sql): centralize fault-injection wire frames; convert decode mocks to real servers #32467 converted sql-mysql-query-string-leak.test.ts from a mock TCP server to a real describeWithContainer("mysql") test; the warmup-batch change (snapshot the RSS baseline after a short identical workload so the measured delta isolates retained strings rather than first-touch heap growth) is carried forward inside the new container-based fixture.

  • test(sql): centralize fault-injection wire frames; convert decode mocks to real servers #32467 also centralized Postgres/MySQL wire-frame builders into test/js/sql/wire-frames.ts; postgres-result-columns.test.ts is rewritten to use pgRowDescription/pgDataRow/pgCommandComplete/pgReadyForQuery/listeningServer instead of its own frame encoders. It stays a mock-server test so the RowDescription→result.columns mapping (including negative system-column attnums) is verified byte-for-byte without needing a container; the real-server coverage is in sql.test.ts.

  • Rebased over the MariaDB extended-type-info work (sql(mysql): negotiate MariaDB extended type info so JSON columns parse into objects #37130): ColumnDefinition41::decode now takes extended_type_info, and main remaps format=json TEXT/BLOB columns to MYSQL_TYPE_JSON during decode. The remap is applied to the decoded type before this PR's change-detection compare, so result.columns[i].type reports the same type code the row decoder uses (245 for MariaDB JSON columns, verified against MariaDB 11.8) and a column flipping to or from JSON format invalidates the cached metadata like any other type change.

  • Rebased over the dead-code and visibility-narrowing passes: FieldDescription.table_oid / column_index had been removed on main as unread and are restored here since this PR is their consumer; ColumnDefinition41.name / table are pub again because bun_sql_jsc reads them; the removed ColumnFlags::to_int is replaced by the bitflags bits() accessor (same from_bits_retain round-trip of the raw wire value); cached_statement_js and the new accessors follow main's pub(crate) convention.

  • The leak test keeps main's rss() measurement helper (test: measure memory via harness rss() instead of process.memoryUsage.rss() #36429, memoryFootprint on macOS) together with this PR's warmup batch.


no test proof · iteration 37 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/sql/sql-mysql-query-string-leak.test.ts

@robobun
robobun requested a review from alii as a code owner May 1, 2026 09:10
@robobun

robobun commented May 1, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Aug 13th, 2026

@robobun, your commit 0043095 is building: #95441

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Bun.SQL: statement and columns field for SQLResultArray #18892 - Also adds .columns and .statement metadata properties to SQLResultArray for exposing PostgreSQL column type information (OIDs) on query results

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This change adds column metadata exposure to SQL query results. Type definitions for ResultColumn and ResultStatement are introduced, along with updates to MySQL and PostgreSQL result handling to compute and attach this metadata. Helper methods serialize column information to JavaScript, and comprehensive tests validate the new API across both database drivers.

Changes

Cohort / File(s) Summary
Type Definitions
packages/bun-types/sql.d.ts
Added ResultColumn and ResultStatement interfaces to expose column metadata (name, type identifier, table OID, flags) and statement-level metadata (final SQL string, columns array) within the bun.SQL namespace.
Shared Result Infrastructure
src/js/internal/sql/shared.ts, src/js/internal/sql/mysql.ts, src/js/internal/sql/postgres.ts
Extended SQLResultArray<T> with columns and statement nullable fields; updated MySQL and PostgreSQL resolution callbacks to accept and conditionally attach statement metadata to results.
MySQL Protocol Layer
src/sql/mysql/MySQLQuery.zig, src/sql/mysql/js/JSMySQLQuery.zig, src/sql/mysql/protocol/ColumnDefinition41.zig
Added getQueryString() accessor; introduced buildStatementJS to construct JS column metadata; extended ColumnDefinition41 with owned string allocations for name/table and toJS() serialization method.
PostgreSQL Protocol Layer
src/sql/postgres/PostgresSQLQuery.zig, src/sql/postgres/PostgresSQLConnection.zig, src/sql/postgres/protocol/FieldDescription.zig
Added buildStatementJS to serialize column metadata; updated result callbacks to pass statement data conditionally; extended FieldDescription with owned name field and toJS() serialization; cleared stale fields during multi-statement simple-mode execution.
MySQL Tests
test/js/sql/sql-mysql.test.ts
Added comprehensive test suite validating result.columns and result.statement surfaces, including column metadata shape (type, length, flags), table-derived OIDs, duplicate aliases, and ownership correctness with long column names.
PostgreSQL Tests
test/js/sql/sql.test.ts, test/js/sql/postgres-result-columns.test.ts
Activated and expanded column metadata tests; added validation for result.columns availability across result modes (unsafe, values, raw, simple), multi-statement behavior, non-SELECT statements, type OID differentiation (jsonb vs text[]), and result.statement.string correctness; introduced mock PostgreSQL server tests for wire-protocol validation.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully addresses both linked issues (#26809, #18866) by exposing column metadata (.columns and .statement) across all query APIs (tagged templates, .unsafe(), .values(), .raw(), .simple()) for PostgreSQL and MySQL.
Out of Scope Changes check ✅ Passed All changes are directly related to exposing column metadata: type definitions, query result augmentation, statement/column serialization logic, wire protocol handling, and comprehensive test coverage.
Title check ✅ Passed The title clearly and concisely describes the main change: exposing column type metadata on SQL query results.
Description check ✅ Passed The description explains the problem, implementation, API changes, supported databases, and verification, covering the template requirements.

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/sql/sql.test.ts`:
- Around line 2998-3003: Add coverage for the zero-field RowDescription emitted
by a bare `SELECT;` in a multi-statement simple query by extending the test for
sql.simple() to include a `select 1 as a; SELECT; select 'x'::text as b, 2::int4
as c` sequence and assert that the middle result has zero columns (empty columns
array) while surrounding results keep their columns/types; additionally, ensure
the implementation that processes RowDescription messages unconditionally resets
the parser state variables `cached_structure`, `needs_duplicate_check`, and
`fields_flags` on every RowDescription handling path (the code that updates
these flags for simple() result sets) so a zero-field RowDescription does not
leave stale cached structure for the next result set.
- Around line 2956-3058: Add tests in the "result.columns / result.statement"
suite that assert .columns and .statement are populated even for zero-row result
sets (e.g. using sql`select 1 as x LIMIT 0` or sql`select 1 as x where false`)
so schema metadata is attached independent of row decoding; specifically call
sql`...` to get a Result, then assert result.columns contains the expected
column descriptors (name and type) and result.statement.string/columns behaves
the same as for non-empty results, ensuring code paths that currently attach
metadata only when rows are decoded are exercised and fixed.

In `@test/regression/issue/26809.test.ts`:
- Around line 64-69: Add a regression case to the existing test "multi-statement
simple() has per-result columns" to cover a zero-column result followed by a
normal result: call sql`SELECT; SELECT 'x'::text as b, 2::int4 as c`.simple()
and assert that results[0].columns maps to an empty array (no names),
results[1].columns.map(c => c.name) equals ["b","c"], and
results[1].columns.map(c => c.type) equals [25,23]; update the test body in the
same test block so the zero-field RowDescription path is exercised before the
next result set.
- Around line 6-11: The current top-level import of dockerCompose causes side
effects on non-Docker runs; change getPostgresURL to lazy-load the module only
when Docker is enabled by replacing the static import usage with a dynamic
import inside the isDockerEnabled() branch (e.g., const dockerCompose = await
import("../../docker/index.ts")) and then call
dockerCompose.ensure("postgres_plain") to obtain info and build the URL; this
keeps the module from being evaluated on non-Docker runs and preserves use of
dockerCompose.ensure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56f3cd53-9da0-40a7-8fef-23fb39fa7d1e

📥 Commits

Reviewing files that changed from the base of the PR and between ba03b9d and f7e99aa.

📒 Files selected for processing (12)
  • packages/bun-types/sql.d.ts
  • src/js/internal/sql/mysql.ts
  • src/js/internal/sql/postgres.ts
  • src/js/internal/sql/shared.ts
  • src/sql/mysql/MySQLQuery.zig
  • src/sql/mysql/js/JSMySQLQuery.zig
  • src/sql/mysql/protocol/ColumnDefinition41.zig
  • src/sql/postgres/PostgresSQLQuery.zig
  • src/sql/postgres/protocol/FieldDescription.zig
  • test/js/sql/sql-mysql.test.ts
  • test/js/sql/sql.test.ts
  • test/regression/issue/26809.test.ts

Comment thread test/js/sql/sql.test.ts
Comment thread test/js/sql/sql.test.ts
Comment thread test/regression/issue/26809.test.ts Outdated
Comment thread test/regression/issue/26809.test.ts Outdated
Comment thread src/sql_jsc/mysql/JSMySQLQuery.zig Outdated
Comment thread test/regression/issue/26809.test.ts Outdated

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/sql/postgres/protocol/FieldDescription.zig (1)

41-72: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add cleanup for field_name on decode failure paths.

ColumnIdentifier.init(name) can allocate, but later fallible reads (or Data.create) can fail before this.* assignment, leaving field_name unfreed on error.

♻️ Proposed fix
-    const field_name = try ColumnIdentifier.init(name);
+    var field_name = try ColumnIdentifier.init(name);
+    errdefer field_name.deinit();

As per coding guidelines, "In Zig code, be careful with allocators and use defer for cleanup".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/sql/postgres/protocol/FieldDescription.zig` around lines 41 - 72, Create
a scoped cleanup for the ColumnIdentifier allocation to avoid leaks on fallible
reads: after calling ColumnIdentifier.init(name) store it into an optional
(e.g., var field_name_opt: ?ColumnIdentifier = try ColumnIdentifier.init(name))
and add a defer that deinitializes it (defer if (field_name_opt) |fn|
fn.deinit();). Continue using reader.* and Data.create as before, and just
before assigning this.* clear ownership by setting field_name_opt = null and
move the value into this.name_or_index (or otherwise transfer ownership) so the
defer won’t free it on success; this ensures field_name is freed on any error
path but retained on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/sql/postgres/protocol/FieldDescription.zig`:
- Around line 41-72: Create a scoped cleanup for the ColumnIdentifier allocation
to avoid leaks on fallible reads: after calling ColumnIdentifier.init(name)
store it into an optional (e.g., var field_name_opt: ?ColumnIdentifier = try
ColumnIdentifier.init(name)) and add a defer that deinitializes it (defer if
(field_name_opt) |fn| fn.deinit();). Continue using reader.* and Data.create as
before, and just before assigning this.* clear ownership by setting
field_name_opt = null and move the value into this.name_or_index (or otherwise
transfer ownership) so the defer won’t free it on success; this ensures
field_name is freed on any error path but retained on success.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cd35c16-edcb-46a3-b3e0-c17650bd1899

📥 Commits

Reviewing files that changed from the base of the PR and between f7e99aa and 5b09db4.

📒 Files selected for processing (2)
  • src/sql/mysql/protocol/ColumnDefinition41.zig
  • src/sql/postgres/protocol/FieldDescription.zig

Comment thread src/sql/mysql/protocol/ColumnDefinition41.zig Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.zig Outdated
Comment thread src/sql/postgres/protocol/FieldDescription.zig Outdated
Comment thread src/sql/mysql/protocol/ColumnDefinition41.zig Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/sql/postgres/PostgresSQLConnection.zig`:
- Around line 1582-1594: The current guard "if (statement.fields.len > 0)"
prevents resetting per-result caches for zero-field RowDescriptions; modify the
logic in the request.flags.simple branch so that freeing/deinit of each field
and bun.default_allocator.free(statement.fields) remain conditional on
statement.fields.len > 0, but always execute
statement.cached_structure.deinit(), set statement.cached_structure = .{}, set
statement.needs_duplicate_check = true, and set statement.fields_flags = .{}
unconditionally (i.e., move those four cache-reset actions out of the fields.len
> 0 block while keeping field deinit/free inside it).

In `@test/js/sql/postgres-result-columns.test.ts`:
- Around line 120-141: Update the "result.columns is populated even for zero-row
result sets" test to also assert that the statement metadata references the same
columns array: after the existing expects for result length and result.columns,
add an assertion that result.statement.columns is exactly the same array as
result.columns (e.g., expect(result.statement.columns).toBe(result.columns)) so
the zero-row invariant is covered; locate this in the test function where result
is obtained from sql`select id, msg from t where false`.simple().

In `@test/js/sql/sql-mysql.test.ts`:
- Around line 79-136: Add a zero-row metadata test to cover the no-row path: run
a query like sql`SELECT CAST(1 AS SIGNED) AS x WHERE FALSE` (or use LIMIT 0) and
assert that result.columns exists, result.columns.map(c => c.name) equals ["x"],
and result.statement.columns === result.columns; add the case alongside the
other column tests (reusing the same sql`...`.values()/.simple() variants as
appropriate) so the code paths in result.columns and Result.statement.columns
for zero-row results are exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8d85a2f3-ba8c-419b-a0d1-182993e72a96

📥 Commits

Reviewing files that changed from the base of the PR and between 37a10c4 and 3319dba.

📒 Files selected for processing (6)
  • src/sql/mysql/protocol/ColumnDefinition41.zig
  • src/sql/postgres/PostgresSQLConnection.zig
  • src/sql/postgres/protocol/FieldDescription.zig
  • test/js/sql/postgres-result-columns.test.ts
  • test/js/sql/sql-mysql.test.ts
  • test/js/sql/sql.test.ts

Comment thread src/sql_jsc/postgres/PostgresSQLConnection.zig Outdated
Comment thread test/js/sql/postgres-result-columns.test.ts
Comment thread test/js/sql/sql-mysql.test.ts
Comment thread src/sql/mysql/protocol/ColumnDefinition41.zig Outdated

@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.

No remaining issues from my side, but this adds new public API surface (.columns/.statement) and touches memory-lifetime-sensitive Zig in both SQL drivers — worth a maintainer pass on the API shape and the Data ownership changes before merging.

Extended reasoning...

Overview

This PR adds result.columns and result.statement to Bun.sql query results, exposing wire-protocol column type metadata (Postgres OIDs, MySQL column-type codes). It spans 13 files: new public TypeScript types in packages/bun-types/sql.d.ts, JS-side resolve-callback plumbing in src/js/internal/sql/{shared,postgres,mysql}.ts, and native Zig changes in both drivers — buildStatementJS() in PostgresSQLQuery.zig / JSMySQLQuery.zig, owned-string lifetime changes in FieldDescription.zig / ColumnDefinition41.zig, and a per-command statement.fields reset in PostgresSQLConnection.zig's .CommandComplete handler. Test coverage includes a new mock-Postgres-server suite, real-Postgres tests in sql.test.ts, and MySQL tests in sql-mysql.test.ts.

Security risks

None identified. The change is read-only metadata exposure derived from server-sent RowDescription / ColumnDefinition41 packets. No new user input is parsed or interpolated into queries; statement.string echoes the already-constructed query text. No auth, crypto, or permission surfaces are touched.

Level of scrutiny

High. This is a new public API whose shape (field names, Postgres vs MySQL divergence, postgres.js compatibility) is a design decision a maintainer should sign off on. More importantly, the native-side changes alter Data ownership semantics on the hot path of every SQL query: over the course of this PR's review, five distinct memory-safety / lifetime issues were found and fixed (UAF on .temporary slices, double-free via aliased .owned ByteList, leak on prepared-statement re-execution, stale fields on multi-statement non-SELECTs, and an errdefer gap). All are now resolved and the latest bug-hunting pass found nothing, but the iteration count signals this code is subtle enough to merit human review.

Other factors

All prior inline comments (mine and CodeRabbit's) are resolved. Test coverage is thorough, including a Docker-free mock-server suite plus ASAN-targeted tests for the >15-byte-name ownership path. CI build #49703 shows musl build failures and two unrelated-looking Windows test failures (astro-post.test.js segfault, hot.test.ts); the musl failures should be confirmed as infra rather than code before merge. buildStatementJS runs unconditionally on every query resolve, so a maintainer may also want to weigh the per-query allocation cost.

@Jarred-Sumner
Jarred-Sumner force-pushed the farm/2d0df50e/sql-result-columns branch from 9d1cfaa to 0040200 Compare May 4, 2026 10:26
Comment thread src/sql/postgres/protocol/FieldDescription.zig Outdated
Comment thread src/js/internal/sql/postgres.ts
Comment thread packages/bun-types/sql.d.ts
@robobun
robobun force-pushed the farm/2d0df50e/sql-result-columns branch from 68e5c98 to 03dcba0 Compare May 4, 2026 11:45

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/bun.js/bindings/GeneratedBindings.zig:1-6 — These two files (and GeneratedJS2Native.zig next to it) are stale codegen artifacts at the legacy pre-restructure src/bun.js/bindings/ path that were accidentally committed — .gitignore only covers the new src/jsc/bindings/ location, so git picked them up. They're ~780 lines of generated code completely unrelated to the SQL column-metadata feature, reference import paths that no longer exist, and are unreferenced by the build; please remove both files (and the now-empty src/bun.js/bindings/ + src/bun.js/ directories) from the PR.

    Extended reasoning...

    What the issue is

    This PR adds two new files at src/bun.js/bindings/GeneratedBindings.zig (391 lines) and src/bun.js/bindings/GeneratedJS2Native.zig (388 lines). Both file headers state they are generated by src/codegen/generate-js2native.ts, and CLAUDE.md's "Generated code" section lists these as build artifacts that should never be committed. They are entirely unrelated to the SQL column-metadata feature this PR implements.

    How they slipped in

    The repo restructure (commit c8b4c36) moved src/bun.js/bindings/src/jsc/bindings/ and updated .gitignore accordingly: lines 128-129 now ignore src/jsc/bindings/GeneratedJS2Native.zig and src/jsc/bindings/GeneratedBindings.zig (the new path), but the old src/bun.js/bindings/ path is no longer covered. A stale codegen run on this branch (likely during the 68e5c98 interface-sync commit) wrote output to the legacy location, and since .gitignore no longer matches it, git add swept the files into the commit.

    Why these files are dead

    • src/bun.js/ now contains only a bindings/ subdirectory containing only these two files — everything else under src/bun.js/ was moved out in the restructure.
    • src/bun.zig imports the generated bindings from ./jsc/bindings/ (the new path), not ./bun.js/bindings/. Grepping the whole repo for bun.js/bindings/Generated returns zero references — nothing in the build system or any source file imports these.
    • The committed files contain stale relative imports like @import("../../bake.zig"), @import("../../crash_handler.zig"), @import("../../sql/mysql.zig"), @import("../../sql/postgres.zig") — none of which exist post-restructure (they are now directories, e.g. src/bake/, src/crash_handler/). If anything did import these files they would fail to compile.
    • Zig only compiles what is reachable from the root module, so the build is unaffected.

    Step-by-step proof

    1. PR diff shows both files as brand-new (--- /dev/null), totalling 779 added lines.
    2. ls -la src/bun.js/ → only bindings/. ls src/bun.js/bindings/ → only GeneratedBindings.zig and GeneratedJS2Native.zig.
    3. .gitignore:128-129src/jsc/bindings/GeneratedJS2Native.zig and src/jsc/bindings/GeneratedBindings.zig. No entry for src/bun.js/bindings/.
    4. src/jsc/bindings/ exists with hundreds of files — it is the actual current bindings location.
    5. GeneratedJS2Native.zig:1//! This file is generated by src/codegen/generate-js2native.ts.
    6. rg 'bun.js/bindings/Generated' across the repo → 0 hits.

    Impact

    No runtime or build impact (the files are unreferenced and Zig's lazy compilation ignores them), but it's 779 lines of accidentally-committed pollution that bloats the diff, obscures review, and would land dead generated code in the tree.

    Fix

    git rm src/bun.js/bindings/GeneratedBindings.zig src/bun.js/bindings/GeneratedJS2Native.zig and remove the now-empty src/bun.js/bindings/ and src/bun.js/ directories. Optionally add src/bun.js/bindings/GeneratedBindings.zig and src/bun.js/bindings/GeneratedJS2Native.zig to .gitignore so other in-flight branches don't hit the same trap.

@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.

No issues found in the latest revision, but this introduces new public API (result.columns / result.statement shapes for PG and MySQL) and changes memory ownership in the Zig protocol decoders, so it's worth a human pass on the API design and the ColumnDefinition41 / FieldDescription / CommandComplete lifecycle changes.

Extended reasoning...

Overview

This PR adds .columns and .statement metadata to Bun.sql query results across PostgreSQL and MySQL, spanning 13 files: public type definitions (sql.d.ts), internal JS resolve callbacks (postgres.ts, mysql.ts, shared.ts), Zig wire-protocol decoders (FieldDescription.zig, ColumnDefinition41.zig), the query/connection layer (PostgresSQLQuery.zig, PostgresSQLConnection.zig, JSMySQLQuery.zig, MySQLQuery.zig), and three test files.

Security risks

None identified. The change is read-only metadata exposure derived from server-sent RowDescription / ColumnDefinition41 packets; no new user input is parsed, no auth/crypto/permissions paths are touched.

Level of scrutiny

High. This is not a mechanical change:

  • It defines new public API surface (Bun.SQL.ResultColumn / ResultStatement) with deliberately different shapes per adapter (PG: {name, type, table, number}; MySQL: {name, type, table, length, flags}). That's a design decision a maintainer should sign off on.
  • It changes memory ownership in hot protocol decoders: ColumnDefinition41.decodeInternal and FieldDescription.decodeInternal now heap-own name/table instead of holding .temporary recv-buffer slices, with new deinit()-before-overwrite calls for the prepared-statement re-decode path.
  • It adds a field-reset block to the Postgres CommandComplete handler for simple-mode multi-statement queries, which alters per-result-set state lifecycle.

Other factors

The PR went through several rounds of memory-safety fixes flagged during review (UAF on temporary slices, a double-free from Data.toOwned() aliasing, a leak on re-decode, a missing errdefer), all of which are now addressed and covered by tests including ASAN-targeted cases. The current bug-hunting pass found nothing. Test coverage is thorough (mock-server wire-protocol tests + real-DB tests for both adapters). The one CI failure (test-http-should-emit-close-when-connection-is-aborted on Windows) is unrelated. Given the API-design and Zig memory-lifecycle aspects, this exceeds the threshold for auto-approval.

@robobun
robobun force-pushed the farm/2d0df50e/sql-result-columns branch from 03dcba0 to 1ec27b0 Compare May 19, 2026 06:12

@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.

No further issues found — all earlier memory-safety findings have been addressed and the Rust port mirrors the Zig changes. Deferring to a human for sign-off since this introduces a new public API surface (result.columns / result.statement) and adds per-query native allocation in both the Zig and Rust SQL drivers.

Extended reasoning...

Overview

This PR adds .columns and .statement metadata to Bun.sql query results, exposing wire-protocol column type information (PostgreSQL OIDs, MySQL type codes) so callers can distinguish e.g. jsonb from text[]. It spans 19 files: public type declarations (sql.d.ts), the JS resolve-callback plumbing (shared.ts, postgres.ts, mysql.ts), native Zig protocol/decode and query-result paths for both Postgres and MySQL, the parallel Rust implementations of the same, and three test files including a new mock-Postgres-server suite.

Security risks

None identified. The change is read-only metadata exposure derived from server-sent RowDescription/ColumnDefinition packets; no new user input reaches a parser or allocator. The strings surfaced to JS are copied via Data.create / createUTF8ForJS rather than aliased from the socket buffer.

Level of scrutiny

High. This is not a mechanical change:

  • It defines a new public API shape (Bun.SQL.ResultColumn / ResultStatement) that downstream tooling (Prisma adapters etc.) will depend on, so the field naming and per-driver semantics deserve a maintainer's eye.
  • It touches memory-sensitive native code in the hot path of every query resolve. Over the course of review, four separate memory-management bugs were found and fixed (UAF on .temporary slices, double-free via Data.toOwned() aliasing for >15-byte names, per-execution leak on prepared-statement re-decode, and stale statement.fields carryover in multi-statement .simple()). All are now addressed with tests, but the density of issues during iteration suggests a human pass over the final ColumnDefinition41.zig / FieldDescription.zig / PostgresSQLConnection.zig ownership model is worthwhile.
  • buildStatementJS now runs unconditionally on every query resolve (Postgres and MySQL), allocating a JS array + N objects per result set even when the caller never reads .columns. That's a deliberate trade-off vs. the lazy-getter approach in the superseded #18892, and a maintainer should confirm it's acceptable.
  • The change is duplicated across Zig and Rust implementations; the Rust port (commit 1ec27b0) was added late and mirrors the Zig logic but hasn't had the same iterative scrutiny.

Other factors

Test coverage is solid: real-Postgres (sql.test.ts), real-MySQL (sql-mysql.test.ts), and a Docker-free mock-server suite (postgres-result-columns.test.ts) covering zero-row results, duplicate names, multi-statement .simple(), signed attnums, and the >15-byte-name ownership case under GC. The two CI failures in build #55988 (node-http-backpressure, jsc-stress) are unrelated to this change. All review threads are resolved.

@robobun
robobun force-pushed the farm/2d0df50e/sql-result-columns branch from c438393 to 0dc95d7 Compare May 22, 2026 22:12

@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.

No outstanding issues from my side — all earlier findings have been addressed. Deferring to a human reviewer since this introduces a new public Bun.SQL API surface across the native Postgres/MySQL drivers and touches packages/bun-types/ (CODEOWNER'd).

Extended reasoning...

Overview

This PR adds .columns and .statement metadata to Bun.sql query results, exposing wire-protocol column type information (PostgreSQL OIDs, MySQL type codes) so driver adapters can distinguish e.g. jsonb from text[]. It spans 19 files: public type definitions (packages/bun-types/sql.d.ts), the internal JS glue (src/js/internal/sql/{shared,postgres,mysql}.ts), native protocol decoders in both Zig and Rust (FieldDescription, ColumnDefinition41), the JSC query bridges (PostgresSQLQuery, JSMySQLQuery, PostgresSQLConnection), and three test files including a new mock-Postgres-server suite.

Security risks

None identified. The change is read-only metadata exposure derived from server-sent RowDescription/ColumnDefinition packets; no new user input is parsed, no auth/crypto/permissions paths are touched, and no new network surface is opened. The memory-lifetime concerns (UAF on .temporary slices, double-free via Data.toOwned() aliasing, leak on prepared-statement re-decode) were all raised during review and fixed in commits 09bb472 / 3319dba / 9d1cfaa with ASAN-verified tests.

Level of scrutiny

High — this warrants human review rather than shadow-approval:

  • New public API design: Bun.SQL.ResultColumn / ResultStatement shapes (PostgreSQL {name, type, table, number} vs MySQL {name, type, table, length, flags}) are being locked in for postgres.js compatibility; a maintainer should sign off on the surface.
  • CODEOWNER'd path: packages/bun-types/sql.d.ts falls under the packages/bun-types/ and *.d.ts CODEOWNERS entries.
  • Native memory management: changes to Data ownership in hot per-query decode paths across two languages (Zig + Rust ports), plus a new per-CommandComplete field-reset in the Postgres simple-query state machine.
  • Per-query overhead: buildStatementJS now runs unconditionally on every result, allocating a JS array + N objects + 2N strings per query even when .columns is never read.

Other factors

The PR has been through several review iterations; every inline finding I raised (status-transition ordering, .temporary UAF, .owned aliasing double-free, re-decode leak, stale-fields inheritance in .simple(), signed i16 attnum, interface-type drift) has been addressed and resolved with targeted tests. The current bug-hunt pass found nothing new. Test coverage is thorough (real-Postgres, real-MySQL, and a Docker-free mock-server suite). The one acknowledged follow-up — wiring ResultColumn[] into a public SQLResult<T> type — is reasonably out of scope here.

Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated
@robobun
robobun force-pushed the farm/2d0df50e/sql-result-columns branch from 9c349a3 to 9021987 Compare May 23, 2026 01:41

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🔴 src/bun_core/debug.rs:1-4 — This file is a 387-line frame-pointer stack unwinder for the crash handler — entirely unrelated to SQL column metadata — and appears to have been accidentally staged into commit 9c349a3. It is also dead code: src/bun_core/lib.rs:3368 already defines pub mod debug { … } as an inline module, so Rust never looks for debug.rs and the file is not compiled. Please drop it from this PR and submit it with the crash-handler work it belongs to.

    Extended reasoning...

    What this is

    src/bun_core/debug.rs is a brand-new 387-line file implementing a frame-pointer stack unwinder: SourceLocation, SymbolInfo, MemoryAccessor, StackIterator, frame_address(), capture_current(), capture_from_context(). Per its own header comment it is a port of std.debug for "the crash handler, StoredTrace, and btjs". This PR is about exposing SQL column type metadata on query results — nothing in the PR title, description, CodeRabbit walkthrough, or any of the 22 other changed files references stack unwinding or crash handling.

    Why it's accidentally committed, not intentional

    git log --diff-filter=A -- src/bun_core/debug.rs shows it was added in commit 9c349a3, whose message is "mysql: invalidate cached statement metadata when column definitions change" — a commit otherwise consisting entirely of changes to ColumnDefinition41.rs, MySQLConnection.rs, MySQLStatement.rs, JSMySQLQuery.rs, and sql-mysql.test.ts. The commit message makes zero mention of debug/stack-trace work. The file does not exist on origin/main. CodeRabbit's "Out of Scope Changes" check passed earlier in the PR, but ran before this commit was pushed (the check timestamp predates 9c349a3). This has all the hallmarks of a stray git add from an unrelated work-in-progress branch.

    Why it's dead code

    src/bun_core/lib.rs is not in this PR's changed files. At line 3368 it already contains:

    pub mod debug {
        /// Zig: `std.debug.SourceLocation`.
        #[derive(Clone)]
        pub struct SourceLocation {
            ...

    This is an inline module (pub mod debug { … } with a body), not a file-module declaration (pub mod debug;). Per Rust's module rules, when a module is defined inline, the compiler does not look for a corresponding debug.rs file — the inline body is the module. So the new src/bun_core/debug.rs is never compiled at all; it's an orphaned file in the source tree. The comment at lib.rs:3367 ("pending a dedicated bun_debug crate") confirms the new file is an in-progress expansion of that inline module that hasn't been wired up yet.

    Step-by-step proof

    1. git log --diff-filter=A -- src/bun_core/debug.rs9c349a3b mysql: invalidate cached statement metadata when column definitions change. The file was added in a commit whose subject and body are exclusively about MySQL statement metadata.
    2. grep -n 'pub mod debug' src/bun_core/lib.rs3368:pub mod debug { (note the opening brace — inline body follows).
    3. src/bun_core/lib.rs is absent from the PR's 23 changed files, so it was not changed to pub mod debug; to point at the new file.
    4. Therefore rustc compiles bun_core::debug from the inline body at lib.rs:3368-… and never reads src/bun_core/debug.rs. The file contributes zero bytes to the build.
    5. Nothing in the SQL changes (src/sql/**, src/sql_jsc/**, src/js/internal/sql/**) references StackIterator, MemoryAccessor, capture_current, or anything else from this file.

    Impact

    No runtime or build impact (the file is uncompiled), but merging it would:

    • Land 387 lines of unrelated, unreviewed crash-handler work under a misleading commit message, making git blame/git log archaeology harder.
    • Create a confusing situation where src/bun_core/debug.rs exists but bun_core::debug resolves to different code in lib.rs.
    • Bypass review of the actual feature (the stack unwinder) by smuggling it in with an unrelated PR.

    Fix

    git rm src/bun_core/debug.rs and amend/rebase it out of this PR. Submit it separately with the crash-handler / bun_debug-crate work it belongs to (where lib.rs would presumably be changed to pub mod debug; to actually use it).

@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.

No issues found in the latest revision — all earlier review feedback has been addressed. That said, this introduces a new public API surface (result.columns / result.statement) and touches lifetime-sensitive native code across both the Zig and Rust SQL drivers, so it's worth a maintainer sign-off on the API shape and the cache-invalidation / ownership changes before merging.

Extended reasoning...

Overview

This PR adds .columns and .statement metadata to Bun.sql query results, exposing wire-protocol column type information (PostgreSQL OIDs, MySQL column-type codes) so driver adapters like Prisma can distinguish e.g. jsonb from text[]. It spans 22 files: public type definitions (sql.d.ts), JS-side resolve-callback plumbing (shared.ts, postgres.ts, mysql.ts), native protocol-layer changes in both Zig and Rust (FieldDescription, ColumnDefinition41, PostgresSQLQuery, JSMySQLQuery, PostgresSQLConnection, MySQLConnection, statement structs), and three test files including a new mock-Postgres-server suite.

Security risks

None identified. The change surfaces metadata the server already sends in RowDescription / ColumnDefinition41 packets; there's no new input parsing, auth, or privilege surface. The data flows server→client only.

Level of scrutiny

High. This is a new public API (the ResultColumn / ResultStatement shape becomes part of Bun's stable surface), and the implementation threads through memory-ownership-sensitive native code in two parallel implementations (Zig + Rust). Over the PR's lifetime the bug-hunting system found and the author fixed: a UAF from .temporary slices outliving the read buffer, a double-free from Data.toOwned() aliasing, a per-execution leak on prepared-statement re-decode, stale-column inheritance across simple-protocol CommandComplete, stale cached metadata for equal-width multi-result-sets, and a signed/unsigned attnum mismatch. All are resolved with regression tests, but the density of lifetime issues found during review is itself a signal that the final state deserves a careful human pass — particularly the new cached_statement_js Strong-reference cache and its invalidation sites, and the ColumnDefinition41::decode change-detection return value.

Other factors

  • Test coverage is thorough: a mock-wire-protocol suite that runs without Docker, plus Docker-gated real-DB tests for both Postgres and MySQL covering zero-row results, duplicate names, multi-statement .simple(), long (>15-byte) column-name ownership, and statement-object reuse.
  • The robobun CI comment lists build failures, but they appear to be pre-existing/unrelated (freebsd unused label, musl build-cpp, -no-pie linker warnings) rather than failures in the changed code.
  • API-design questions a maintainer may want to weigh in on: the per-dialect ResultColumn shape (Postgres uses table: number, MySQL uses table: string; Postgres has number, MySQL has length/flags), and whether the { string, columns } object should be cached/shared across executions vs. fresh per query (this PR caches, matching postgres.js semantics).

Comment thread src/sql_jsc/mysql/MySQLStatement.rs Outdated
Comment thread src/sql/postgres/protocol/FieldDescription.zig Outdated
@mintlify

mintlify Bot commented Jun 23, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bun 🟢 Ready View Preview Jun 23, 2026, 5:20 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@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.

No issues found in this pass. Given this introduces a new public API surface (result.columns / result.statement) and touches native statement caching / memory ownership across both SQL drivers, it's worth a human sign-off before merge.

Extended reasoning...

Overview

This PR adds .columns and .statement metadata to Bun.sql query results, exposing wire-protocol column type information (PostgreSQL OIDs / MySQL type codes) so driver adapters can distinguish e.g. jsonb from text[]. It spans 18 files: public type definitions (sql.d.ts), prose docs (sql.mdx), JS-side result plumbing (shared.ts/postgres.ts/mysql.ts), native Rust changes to both drivers' protocol decoding, query/statement objects, and connection state machines (FieldDescription.rs, ColumnDefinition41.rs, PostgresSQLQuery.rs, JSMySQLQuery.rs, PostgresSQLConnection.rs, MySQLConnection.rs, *Statement.rs), plus a new mock-server test file and additions to the docker-gated suites.

Security risks

None identified. The change is read-only metadata exposure derived from data the server already sends; no new user input is parsed, no auth/crypto/permissions paths are touched, and the SQL text in statement.string is the already-parameterized string the client itself constructed.

Level of scrutiny

High. This is not a mechanical change:

  • It defines a new, user-facing, stable API surface (shape of ResultColumn / ResultStatement) that adapters like Prisma will depend on.
  • It adds a StrongOptional JSC-reference cache (cached_statement_js) on native statement objects with non-trivial invalidation rules tied to wire-protocol re-decode paths in both drivers.
  • It changes ownership semantics of Data fields in ColumnDefinition41 / FieldDescription (temporary→owned copies) on hot decode paths.
  • Earlier review rounds on this PR surfaced and fixed several memory-safety issues (UAF on temporary read-buffer slices, double-free via aliased Data::Owned, per-execution leaks on re-decode, stale-cache correctness for equal-width multi-result-sets). All are resolved and now have regression tests, but the density of such findings indicates the change sits in tricky territory.

Other factors

All 17 prior inline review threads (mine and CodeRabbit's) are resolved; the bug-hunting pass on the current revision found nothing. Test coverage is thorough — a Docker-free mock-Postgres-server suite plus docker-gated PostgreSQL/MySQL/MariaDB tests covering zero-row, duplicate-name, multi-statement .simple(), stale-column inheritance, long-name ownership, and prepared-statement metadata reuse. The PR description documents that the per-statement caching keeps the issue-28632 RSS regression test within its ASAN threshold. CI is building on the latest rebase. Net: the PR looks correct and well-tested, but the scope (new public API + native lifetime/caching across two drivers) is beyond what I'd auto-approve.

@KilianB

KilianB commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

@robobun One of buns strengths is performance and throughput, wouldn't this be a performance hit? Should attaching this information be opt in instead?

Adds result.columns and result.statement to Bun.sql query results for
PostgreSQL (pg_type OIDs) and MySQL (column-type codes), with per-statement
caching of the statement JS object and invalidation when column definitions
change.
@robobun
robobun force-pushed the farm/2d0df50e/sql-result-columns branch from 46ac248 to 31e779f Compare August 14, 2026 03:05
Comment thread src/sql/mysql/protocol/ColumnDefinition41.rs Outdated
Comment thread src/sql/mysql/protocol/ColumnDefinition41.rs Outdated
Comment thread src/sql/mysql/protocol/ColumnDefinition41.rs Outdated
Comment thread src/sql/postgres/protocol/FieldDescription.rs Outdated
Comment thread src/sql/postgres/protocol/FieldDescription.rs Outdated
Comment thread src/sql_jsc/mysql/JSMySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/JSMySQLQuery.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLConnection.rs Outdated
Comment thread src/sql_jsc/mysql/MySQLStatement.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLConnection.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLQuery.rs Outdated
Comment thread src/sql_jsc/postgres/PostgresSQLStatement.rs Outdated

@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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/sql/mysql/protocol/ColumnDefinition41.rs:121-143 — The comment-cop bot has 14 unresolved flags on this PR, and they're right: several new comment blocks exceed REVIEW.md's "one line" rule and duplicate each other — most visibly here, where two consecutive blocks (~21 lines for ~5 lines of code) both restate "Column definitions are re-decoded into the same slot on every COM_STMT_EXECUTE", with a third repetition at ~line 230. The same cache-policy rationale is also documented three times per driver (field doc on cached_statement_js, the 13-16 line build_statement_js doc, and the resolve/on_result call-site comment). Suggest keeping the full rationale once on the cached_statement_js field doc and collapsing the rest to one-line issue links, e.g. // oven-sh/bun#28632: skip re-copy when unchanged so repeated executions stay allocation-flat.

    Extended reasoning...

    What the issue is

    REVIEW.md ("Code style & idioms reviewers enforce") states:

    Only comment what the code cannot say. One line. Never restate what the code does. Never narrate the change. Prefer links to GitHub issues.

    CLAUDE.md #13/#14 add: "If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong" and "Is this information the next Claude would spend multiple tool calls trying to understand? If not, delete it."

    The repo's own comment-cop GitHub Action has posted 14 unresolved inline comments on this PR (2026-08-14) at exactly these locations, each quoting CLAUDE.md #13. Independent verification confirms each flagged block is a multi-line prose comment that either narrates the change or duplicates rationale documented elsewhere in the same PR.

    The worst case: ColumnDefinition41.rs:121-143

    Two consecutive comment blocks preceding let table = reader.encode_len_string()?;:

    • Lines 121-129 (9 lines): explains changed tracks whether any field surfaced in result.columns differs, that "Column definitions are re-decoded into the same slot on every COM_STMT_EXECUTE / result set", and gives the SELECT 1 AS x; SELECT 'hi' AS x example plus the test/regression/issue/28632 link.
    • Lines 132-143 (12 lines): explains why name/table are owned copies, then repeats "Column definitions are re-decoded into the same slot on every COM_STMT_EXECUTE of a reused prepared statement" and re-cites test/regression/issue/28632.
    • Lines ~226-235 (pre-existing on main, 10 lines): a third block repeating "The server re-sends column definitions on every COM_STMT_EXECUTE, so a reused prepared statement re-decodes into the same slot once per query… shows up as steady RSS growth under the ASAN quarantine (test/regression/issue/28632)."

    That's ~21 lines of new comment for ~5 lines of code, restating the same fact three times within one function.

    The duplication is also cross-file

    The cache-policy rationale ("built once per prepared statement, held via a Strong reference, reused across executions, invalidated when column definitions change, never cached for text-protocol/simple queries because pinning would keep large query strings alive") is documented in three places per driver:

    Location MySQL Postgres
    Field doc on cached_statement_js MySQLStatement.rs:23-29 (7 lines) PostgresSQLStatement.rs:22-26 (5 lines)
    build_statement_js fn doc JSMySQLQuery.rs:237-252 (16 lines) PostgresSQLQuery.rs:294-306 (13 lines)
    Call-site comment in resolve/on_result JSMySQLQuery.rs:356-361 (6 lines) PostgresSQLQuery.rs:403-408 (6 lines)

    Other flagged sites: FieldDescription.rs:8-10, 33-34; MySQLConnection.rs:1274-1276; PostgresSQLConnection.rs:2537-2543, 2628-2629; PostgresSQLQuery.rs:336-338.

    Step-by-step proof

    1. grep -n 're-decoded into the same slot' src/sql/mysql/protocol/ColumnDefinition41.rs → hits at lines ~123, ~139; grep -n 're-decodes into the same slot' → hit at ~230. Three occurrences of the same rationale in one function.
    2. grep -n 'test/regression/issue/28632' src/sql/mysql/protocol/ColumnDefinition41.rs → 3 hits in one file; also cited in MySQLStatement.rs and JSMySQLQuery.rs.
    3. The build_statement_js doc comment at JSMySQLQuery.rs:237-252 is 16 lines; the field doc it duplicates at MySQLStatement.rs:23-29 already says the same thing in 7.
    4. The PR timeline shows 14 unresolved github-actions comment-cop inline comments dated 2026-08-14, one per flagged block — the repo's own automation agrees.

    Why existing code doesn't prevent it

    Nothing in the build or CI enforces comment length; comment-cop only posts inline warnings. The blocks were written incrementally in response to earlier review threads (each fix commit added another explanatory paragraph rather than editing the existing one), which is how the same sentence ended up written three times.

    Impact

    Zero runtime impact — this is comment hygiene only. But it violates an explicit repo rule enforced by an automated check, and the duplication makes the file harder to maintain: the next person editing the invalidation logic has to keep three prose blocks in sync (they've already drifted once — c3b26dc had to fix a stale to_js() reference in one of them).

    Suggested fix

    Keep the full rationale in exactly one place — the field-level /// doc on cached_statement_js in {MySQL,PostgresSQL}Statement.rs — and collapse each of the other 12 sites to a single line linking the issue, e.g.:

    // oven-sh/bun#28632: skip re-copy when unchanged so repeated executions stay allocation-flat.
    let table = reader.encode_len_string()?;

    For ColumnDefinition41.rs specifically, the two adjacent blocks at 121-143 can become one line each ("changed reports whether any result.columns field differs — see MySQLStatement::cached_statement_js" and "own a copy: Data::Temporary slices into the read buffer are invalidated before build_statement_js reads them"), and the third block at ~226-235 can shrink to "skip rebuild when unchanged (#28632)".

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed in a8c1c4b (pushed just before this review ran against 31e779f): every comment block this PR adds under src/ is now a single line, each site keeps one distinct fact (field doc: lifetime of the cache; build_statement_js: why one-shot statements are not cached; call site: why a failed build resolves instead of rejecting), and the comment-cop run on a8c1c4b resolved all 15 of its threads with nothing new flagged.

The third block mentioned around line 226 (the name_or_index rebuild elision) is existing code on main, not part of this diff, so it is left as is.

Comment thread src/sql_jsc/mysql/MySQLConnection.rs
…ecode

The row structure and duplicate check depend only on name_or_index, while
the cached statement object depends on the raw name, table, type, length and
flags. Returning the two signals separately keeps a type-only change from
rebuilding the row structure, and a duplicate-name re-execution from
rebuilding the statement object. The prepare path only ever fills freshly
allocated slots, so it has nothing to invalidate.
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up from #38443: the existing public count!: ... class fields in SQLResultArray are what make the result metadata enumerable (the field is defined before Object.defineProperties runs, so the descriptor only updates the value). #38443 turns them into declare fields and spells out enumerable: false in the descriptors. The columns / statement fields this PR adds follow the old pattern, so when rebasing onto it, please make them declare fields too and copy the new descriptor shape. If they stay real class fields with { value, writable } descriptors, the new Object.keys(result) assertion in test/js/sql/sqlite-sql.test.ts will fail.

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.

Expose column type metadata (OIDs) on PostgreSQL query results Bun.SQL result has properties missing compared to Postgres.js

2 participants