fix(drizzle): remap object rows to TS field names for all dialects#227
fix(drizzle): remap object rows to TS field names for all dialects#227meyer1994 wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis PR fixes Drizzle column name remapping in db0 by adding field-aware row transformation utilities and threading them through the MySQL, PostgreSQL, and SQLite session layers. Result rows now properly convert database column names (e.g., ChangesDrizzle Column Name Remapping
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/integrations/drizzle/_utils.ts`:
- Around line 8-10: The current fields.map callback in
src/integrations/drizzle/_utils.ts uses row[field.name] for Column lookups,
which loses values for joined/duplicate-name projections; update the mapping to
derive the result key from dialect-aware select aliases instead of falling back
to field.name by (1) using the alias provided by SQL.Aliased when present, (2)
consulting the SQLDialect-aware aliasing strategy used by the connector (e.g., a
helper that builds column select aliases per SQLDialect) to compute the lookup
key for Column instances instead of field.name, and (3) ensuring mapRow (or the
code path that builds select projections) generates and exposes those aliases on
the row objects so fields.map can use that key reliably across dialects and
avoid nulling duplicate-named columns.
In `@src/integrations/drizzle/sqlite/_session.ts`:
- Around line 200-208: The values() method unconditionally dereferences
this.fields and throws for raw prepared queries lacking selected-field metadata;
replicate the no-field short-circuit used by all() and get() by returning rows
mapped to Object.values (or equivalent row array) when this.fields is
null/undefined. Update values() (and its use of rowToArray) to check this.fields
first—if absent, convert each row to an array of values in insertion order;
otherwise continue using rowToArray(this.fields, row) so existing behavior for
known fields remains unchanged.
In `@test/integrations/drizzle/mysql.test.ts`:
- Around line 115-125: The test is flaky because drizzleDb.select().from(events)
returns rows in unspecified order; make the assertion deterministic by adding an
ORDER BY to the query so the first row will always be the one with fooBar === 1
(e.g., call .orderBy(events.fooBar) or another stable column like events.id or
events.createdAt), then keep the assertions against res[0]; update the test case
("select returns camelCase keys, not snake_case") to use the ordered query so
res[0].fooBar consistently equals 1.
In `@test/integrations/drizzle/pg.test.ts`:
- Around line 144-154: The test relies on implicit row ordering; change the
query built by drizzleDb.select().from(events) to include an explicit ORDER BY
on a stable column (e.g., events.fooBar or events.createdAt) so the row with
fooBar = 1 is deterministically first; update the call to use the library's
orderBy API (and specify ascending or descending as needed) and adjust the
assertion if you choose the opposite sort direction.
In `@test/integrations/drizzle/sqlite.test.ts`:
- Around line 201-211: The test relies on an unordered result so change the
query in the test that calls drizzleDb.select().from(events).all() to include an
explicit order to make the assertion deterministic; update the select call (in
the test case `"select returns camelCase keys, not snake_case"`) to add an
.orderBy(...) on a stable column (e.g., events.id or events.createdAt) so res[0]
will always be the row with fooBar = 1 and the subsequent property assertions
remain valid.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: bde52cd6-bc86-41f5-a0d9-f7669b17aa8a
📒 Files selected for processing (7)
src/integrations/drizzle/_utils.tssrc/integrations/drizzle/mysql/_session.tssrc/integrations/drizzle/postgres/_session.tssrc/integrations/drizzle/sqlite/_session.tstest/integrations/drizzle/mysql.test.tstest/integrations/drizzle/pg.test.tstest/integrations/drizzle/sqlite.test.ts
| return fields.map(({ field }) => { | ||
| if (is(field, Column)) return row[field.name]; | ||
| if (is(field, SQL.Aliased)) return row[field.fieldAlias]; |
There was a problem hiding this comment.
field.name is not a safe lookup key for shared object-row mapping.
These helpers assume every Column comes back under row[field.name]. That still breaks joined or duplicate-name projections (users.id + posts.id, etc.): the object row can only retain one key, so one value is lost before remapping, and mapRow() then silently turns the missing lookup into null. Because this utility is shared by all three connectors, the fix needs dialect-aware select aliases / result keys rather than a backend-agnostic field.name fallback.
As per coding guidelines, src/**/*.ts: Database dialect behavior must adjust SQL generation per SQLDialect (e.g., RETURNING support varies by backend).
Also applies to: 22-29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/integrations/drizzle/_utils.ts` around lines 8 - 10, The current
fields.map callback in src/integrations/drizzle/_utils.ts uses row[field.name]
for Column lookups, which loses values for joined/duplicate-name projections;
update the mapping to derive the result key from dialect-aware select aliases
instead of falling back to field.name by (1) using the alias provided by
SQL.Aliased when present, (2) consulting the SQLDialect-aware aliasing strategy
used by the connector (e.g., a helper that builds column select aliases per
SQLDialect) to compute the lookup key for Column instances instead of
field.name, and (3) ensuring mapRow (or the code path that builds select
projections) generates and exposes those aliases on the row objects so
fields.map can use that key reliably across dialects and avoid nulling
duplicate-named columns.
| async values<T extends any[] = unknown[]>( | ||
| placeholderValues?: Record<string, unknown>, | ||
| ): Promise<T[]> { | ||
| const params = fillPlaceholders(this.query.params, placeholderValues ?? {}); | ||
| const placeholders = placeholderValues ?? {}; | ||
| const params: any[] = fillPlaceholders(this.query.params, placeholders); | ||
| this.logger.logQuery(this.query.sql, params); | ||
| const rows = await this.stmt.all(...(params as any[])); | ||
| // db0 Statement doesn't have a values() method, so convert object rows to arrays | ||
| return (rows as Record<string, unknown>[]).map( | ||
| (row) => Object.values(row) as T, | ||
| ); | ||
|
|
||
| const rows = (await this.stmt.all(...params)) as Record<string, unknown>[]; | ||
| return rows.map((row) => rowToArray(this.fields!, row) as T); |
There was a problem hiding this comment.
values() lost the no-field fallback.
all() and get() still short-circuit when fields is absent, but values() unconditionally dereferences this.fields!. Any raw prepared query that reaches .values() without selected-field metadata will now throw here instead of returning row values.
Suggested fix
async values<T extends any[] = unknown[]>(
placeholderValues?: Record<string, unknown>,
): Promise<T[]> {
const placeholders = placeholderValues ?? {};
const params: any[] = fillPlaceholders(this.query.params, placeholders);
this.logger.logQuery(this.query.sql, params);
const rows = (await this.stmt.all(...params)) as Record<string, unknown>[];
- return rows.map((row) => rowToArray(this.fields!, row) as T);
+ if (!this.fields) {
+ return rows.map((row) => Object.values(row) as T);
+ }
+
+ return rows.map((row) => rowToArray(this.fields, row) as T);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| async values<T extends any[] = unknown[]>( | |
| placeholderValues?: Record<string, unknown>, | |
| ): Promise<T[]> { | |
| const params = fillPlaceholders(this.query.params, placeholderValues ?? {}); | |
| const placeholders = placeholderValues ?? {}; | |
| const params: any[] = fillPlaceholders(this.query.params, placeholders); | |
| this.logger.logQuery(this.query.sql, params); | |
| const rows = await this.stmt.all(...(params as any[])); | |
| // db0 Statement doesn't have a values() method, so convert object rows to arrays | |
| return (rows as Record<string, unknown>[]).map( | |
| (row) => Object.values(row) as T, | |
| ); | |
| const rows = (await this.stmt.all(...params)) as Record<string, unknown>[]; | |
| return rows.map((row) => rowToArray(this.fields!, row) as T); | |
| async values<T extends any[] = unknown[]>( | |
| placeholderValues?: Record<string, unknown>, | |
| ): Promise<T[]> { | |
| const placeholders = placeholderValues ?? {}; | |
| const params: any[] = fillPlaceholders(this.query.params, placeholders); | |
| this.logger.logQuery(this.query.sql, params); | |
| const rows = (await this.stmt.all(...params)) as Record<string, unknown>[]; | |
| if (!this.fields) { | |
| return rows.map((row) => Object.values(row) as T); | |
| } | |
| return rows.map((row) => rowToArray(this.fields, row) as T); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/integrations/drizzle/sqlite/_session.ts` around lines 200 - 208, The
values() method unconditionally dereferences this.fields and throws for raw
prepared queries lacking selected-field metadata; replicate the no-field
short-circuit used by all() and get() by returning rows mapped to Object.values
(or equivalent row array) when this.fields is null/undefined. Update values()
(and its use of rowToArray) to check this.fields first—if absent, convert each
row to an array of values in insertion order; otherwise continue using
rowToArray(this.fields, row) so existing behavior for known fields remains
unchanged.
| it("select returns camelCase keys, not snake_case", async () => { | ||
| const res = await drizzleDb.select().from(events); | ||
| expect(res.length).toBe(2); | ||
| expect(res[0]).toHaveProperty("fooBar"); | ||
| expect(res[0]).toHaveProperty("createdAt"); | ||
| expect(res[0]).toHaveProperty("userFullName"); | ||
| expect(res[0]).not.toHaveProperty("foo_bar"); | ||
| expect(res[0]).not.toHaveProperty("created_at"); | ||
| expect(res[0]).not.toHaveProperty("user_full_name"); | ||
| expect(res[0].fooBar).toBe(1); | ||
| }); |
There was a problem hiding this comment.
Make the first-row assertion deterministic.
Line 116 selects rows without ordering, but Line 124 asserts res[0].fooBar === 1. Row order is not guaranteed without ORDER BY, so this can become flaky.
Suggested fix
- const res = await drizzleDb.select().from(events);
+ const res = await drizzleDb.select().from(events).orderBy(events.id);
expect(res.length).toBe(2);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); | |
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events).orderBy(events.id); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/integrations/drizzle/mysql.test.ts` around lines 115 - 125, The test is
flaky because drizzleDb.select().from(events) returns rows in unspecified order;
make the assertion deterministic by adding an ORDER BY to the query so the first
row will always be the one with fooBar === 1 (e.g., call .orderBy(events.fooBar)
or another stable column like events.id or events.createdAt), then keep the
assertions against res[0]; update the test case ("select returns camelCase keys,
not snake_case") to use the ordered query so res[0].fooBar consistently equals
1.
| it("select returns camelCase keys, not snake_case", async () => { | ||
| const res = await drizzleDb.select().from(events); | ||
| expect(res.length).toBe(2); | ||
| expect(res[0]).toHaveProperty("fooBar"); | ||
| expect(res[0]).toHaveProperty("createdAt"); | ||
| expect(res[0]).toHaveProperty("userFullName"); | ||
| expect(res[0]).not.toHaveProperty("foo_bar"); | ||
| expect(res[0]).not.toHaveProperty("created_at"); | ||
| expect(res[0]).not.toHaveProperty("user_full_name"); | ||
| expect(res[0].fooBar).toBe(1); | ||
| }); |
There was a problem hiding this comment.
Avoid depending on implicit row order.
Line 145 fetches rows without ORDER BY, but Line 153 expects the first row to be the fooBar = 1 row. That assumption is unstable across PostgreSQL implementations.
Suggested fix
- const res = await drizzleDb.select().from(events);
+ const res = await drizzleDb.select().from(events).orderBy(events.id);
expect(res.length).toBe(2);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); | |
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events).orderBy(events.id); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/integrations/drizzle/pg.test.ts` around lines 144 - 154, The test relies
on implicit row ordering; change the query built by
drizzleDb.select().from(events) to include an explicit ORDER BY on a stable
column (e.g., events.fooBar or events.createdAt) so the row with fooBar = 1 is
deterministically first; update the call to use the library's orderBy API (and
specify ascending or descending as needed) and adjust the assertion if you
choose the opposite sort direction.
| it("select returns camelCase keys, not snake_case", async () => { | ||
| const res = await drizzleDb.select().from(events).all(); | ||
| expect(res.length).toBe(2); | ||
| expect(res[0]).toHaveProperty("fooBar"); | ||
| expect(res[0]).toHaveProperty("createdAt"); | ||
| expect(res[0]).toHaveProperty("userFullName"); | ||
| expect(res[0]).not.toHaveProperty("foo_bar"); | ||
| expect(res[0]).not.toHaveProperty("created_at"); | ||
| expect(res[0]).not.toHaveProperty("user_full_name"); | ||
| expect(res[0].fooBar).toBe(1); | ||
| }); |
There was a problem hiding this comment.
Stabilize this assertion with explicit ordering.
Line 202 returns an unordered result set, while Line 210 assumes res[0] is the row with fooBar = 1. Add an order clause to keep this test deterministic.
Suggested fix
- const res = await drizzleDb.select().from(events).all();
+ const res = await drizzleDb.select().from(events).orderBy(events.id).all();
expect(res.length).toBe(2);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events).all(); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); | |
| it("select returns camelCase keys, not snake_case", async () => { | |
| const res = await drizzleDb.select().from(events).orderBy(events.id).all(); | |
| expect(res.length).toBe(2); | |
| expect(res[0]).toHaveProperty("fooBar"); | |
| expect(res[0]).toHaveProperty("createdAt"); | |
| expect(res[0]).toHaveProperty("userFullName"); | |
| expect(res[0]).not.toHaveProperty("foo_bar"); | |
| expect(res[0]).not.toHaveProperty("created_at"); | |
| expect(res[0]).not.toHaveProperty("user_full_name"); | |
| expect(res[0].fooBar).toBe(1); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/integrations/drizzle/sqlite.test.ts` around lines 201 - 211, The test
relies on an unordered result so change the query in the test that calls
drizzleDb.select().from(events).all() to include an explicit order to make the
assertion deterministic; update the select call (in the test case `"select
returns camelCase keys, not snake_case"`) to add an .orderBy(...) on a stable
column (e.g., events.id or events.createdAt) so res[0] will always be the row
with fooBar = 1 and the subsequent property assertions remain valid.
Fixes #196
I encountered this issue while trying to make drizzle db0 integration work with better-auth drizzle adapter
db0 connectors return object rows
({ foo_bar: 1 })instead of the positional array rows drizzle's internals expect. Previously the fields parameter was discarded andisResponseInArrayMode()returnedfalse, so drizzle's remapping logic was never triggered and queries returned raw snake_case column names instead of the camelCase TSproperty names defined in the schema.
Changes:
_utils.tswith sharedrowToArrayandmapRowhelpers that translate db0 object rows into drizzle's expected formatsisResponseInArrayMode()to true, and apply the row mappers inall(),get(),execute(), andvalues()eq(),get(),insert().returning(), and full key assertionsSummary by CodeRabbit
Release Notes
Bug Fixes
Tests