Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/integrations/drizzle/_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { is, Column, SQL } from "drizzle-orm";
import type { SelectedFieldsOrdered } from "drizzle-orm/operations";

// Note: db0 connectors always return object rows keyed by the driver's column
// name. Queries selecting two columns with the same name (e.g. `users.id` and
// `posts.id` in a join) therefore collapse to a single key and cannot be
// disambiguated at this layer — a driver-level array/raw row mode would be
// required. Drizzle's relational query builder avoids this by emitting unique
// aliases, so relational queries are unaffected.

export function rowToArray(
fields: SelectedFieldsOrdered<Column> | undefined,
row: Record<string, unknown>,
): unknown[] {
// The relational query builder passes no `fields` alongside its
// `customResultMapper`; db0 rows are objects, so hand back the values in
// SELECT order (object key insertion order matches the generated SQL).
if (!fields) {
return Object.values(row);
}
const values = Object.values(row);
return fields.map(({ field }, index) => {
if (is(field, Column)) return row[field.name];
if (is(field, SQL.Aliased)) return row[field.fieldAlias];
// Bare (non-aliased) SQL expression: db0 keys it by the raw expression text
// (e.g. `count(*)`), so fall back to positional lookup — object key order
// matches the SELECT order.
return values[index];
});
}

export function mapRow(
fields: SelectedFieldsOrdered<Column>,
row: Record<string, unknown>,
): Record<string, unknown> {
const result: Record<string, unknown> = {};
const values = Object.values(row);
for (const [index, { path, field }] of fields.entries()) {
let rawValue: unknown;
if (is(field, Column)) {
rawValue = row[field.name];
} else if (is(field, SQL.Aliased)) {
rawValue = row[field.fieldAlias];
} else {
// Bare SQL expression: db0 keys it by the raw expression text, so fall
// back to positional lookup (object key order matches SELECT order).
rawValue = values[index];
}
let value: unknown;
if (rawValue == null) {
value = null;
} else if (is(field, Column)) {
value = field.mapFromDriverValue(rawValue);
} else {
value = rawValue;
}
let node = result;
for (const [i, chunk] of path.entries()) {
if (i < path.length - 1) {
node = (node[chunk] as Record<string, unknown>) ??= {};
} else {
node[chunk] = value;
}
}
}
return result;
}
17 changes: 9 additions & 8 deletions src/integrations/drizzle/mysql/_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import {
type RelationalSchemaConfig,
type Query,
type TablesRelationalConfig,
type SQL,
NoopLogger,
fillPlaceholders,
sql,
} from "drizzle-orm";

import { rowToArray, mapRow } from "../_utils.ts";

import {
MySqlDialect,
MySqlSession,
Expand All @@ -25,8 +28,6 @@ import type {
Mode,
} from "drizzle-orm/mysql-core";

import type { SQL } from "drizzle-orm";

import type { Database } from "db0";

export interface DB0MySqlSessionOptions {
Expand Down Expand Up @@ -178,23 +179,23 @@ export class DB0MySqlPreparedQuery<
async execute(
placeholderValues: Record<string, unknown> | undefined = {},
): Promise<T["execute"]> {
const params = fillPlaceholders(this.params, placeholderValues);
const params: any[] = fillPlaceholders(this.params, placeholderValues);
this.logger.logQuery(this.queryString, params);

const stmt = this.db.prepare(this.queryString);

if (!this.fields && !this.customResultMapper) {
return stmt.all(...(params as any[]));
return stmt.all(...params);
}

const rows = await stmt.all(...(params as any[]));
const rows = (await stmt.all(...params)) as Record<string, unknown>[];

if (this.customResultMapper) {
return this.customResultMapper(rows as unknown[][]);
const arr = rows.map((row) => rowToArray(this.fields, row));
return this.customResultMapper(arr);
}

// db0 returns object rows, return as-is when fields are present
return rows as T["execute"];
return rows.map((row) => mapRow(this.fields!, row)) as T["execute"];
}

// eslint-disable-next-line require-yield
Expand Down
34 changes: 24 additions & 10 deletions src/integrations/drizzle/postgres/_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
sql,
} from "drizzle-orm";

import { rowToArray, mapRow } from "../_utils.ts";

import {
PgDialect,
PgPreparedQuery,
Expand Down Expand Up @@ -147,34 +149,46 @@ export class DB0PgPreparedQuery<
async execute(
placeholderValues: Record<string, unknown> | undefined = {},
): Promise<T["execute"]> {
const params = fillPlaceholders(this.params, placeholderValues);
const params: any[] = fillPlaceholders(this.params, placeholderValues);
this.logger.logQuery(this.queryString, params);

const stmt = this.db.prepare(this.queryString);

if (!this.fields && !this.customResultMapper) {
return stmt.all(...(params as any[]));
return stmt.all(...params);
}

const rows = await stmt.all(...(params as any[]));
const rows = (await stmt.all(...params)) as Record<string, unknown>[];

if (this.customResultMapper) {
return this.customResultMapper(rows as unknown[][]);
const arr = rows.map((row) => rowToArray(this.fields, row));
return this.customResultMapper(arr);
}

// db0 returns object rows, return as-is when fields are present
return rows as T["execute"];
return rows.map((row) => mapRow(this.fields!, row)) as T["execute"];
}

async all(): Promise<T["all"]> {
const params = this.params as any[];
this.logger.logQuery(this.queryString, params);
const stmt = this.db.prepare(this.queryString);
this.logger.logQuery(this.queryString, this.params);
return stmt.all(...(this.params as any[])) as Promise<T["all"]>;

if (!this.fields && !this.customResultMapper) {
return stmt.all(...params) as Promise<T["all"]>;
}

const rows = (await stmt.all(...params)) as Record<string, unknown>[];

if (this.customResultMapper) {
const arr = rows.map((row) => rowToArray(this.fields, row));
return this.customResultMapper(arr) as T["all"];
}

return rows.map((row) => mapRow(this.fields!, row)) as T["all"];
}

/** @internal */
isResponseInArrayMode(): boolean {
// db0 always returns object rows, never array rows
return false;
return true;
}
}
62 changes: 48 additions & 14 deletions src/integrations/drizzle/sqlite/_session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
sql,
} from "drizzle-orm";

import { rowToArray, mapRow } from "../_utils.ts";

import {
SQLiteAsyncDialect,
SQLiteSession,
Expand Down Expand Up @@ -50,7 +52,7 @@ export class DB0SQLiteSession<
query: Query,
fields: SelectedFieldsOrdered | undefined,
executeMethod: SQLiteExecuteMethod,
_isResponseInArrayMode: boolean,
isResponseInArrayMode: boolean,
customResultMapper?: (rows: unknown[][]) => unknown,
): DB0SQLitePreparedQuery {
const stmt = this.db.prepare(query.sql);
Expand All @@ -60,6 +62,7 @@ export class DB0SQLiteSession<
this.logger,
fields,
executeMethod,
isResponseInArrayMode,
customResultMapper,
);
}
Expand Down Expand Up @@ -130,15 +133,21 @@ export class DB0SQLitePreparedQuery<
values: T["values"];
execute: T["execute"];
}> {
private fields: SelectedFieldsOrdered | undefined;
private isResponseInArrayMode_: boolean;

constructor(
private stmt: Statement,
query: Query,
private logger: Logger,
_fields: SelectedFieldsOrdered | undefined,
fields: SelectedFieldsOrdered | undefined,
executeMethod: SQLiteExecuteMethod,
isResponseInArrayMode: boolean,
/** @internal */ public customResultMapper?: (rows: unknown[][]) => unknown,
) {
super("async", executeMethod, query);
this.fields = fields;
this.isResponseInArrayMode_ = isResponseInArrayMode;
}

async run(
Expand All @@ -150,32 +159,57 @@ export class DB0SQLitePreparedQuery<
}

async all(placeholderValues?: Record<string, unknown>): Promise<T["all"]> {
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);
return this.stmt.all(...(params as any[]));

if (!this.fields && !this.customResultMapper) {
return this.stmt.all(...params) as T["all"];
}

const rows = (await this.stmt.all(...params)) as Record<string, unknown>[];

if (this.customResultMapper) {
const arr = rows.map((row) => rowToArray(this.fields, row));
return this.customResultMapper(arr) as T["all"];
}

return rows.map((row) => mapRow(this.fields!, row)) as T["all"];
}

async get(placeholderValues?: Record<string, unknown>): Promise<T["get"]> {
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);
return this.stmt.get(...(params as any[]));

if (!this.fields && !this.customResultMapper) {
return this.stmt.get(...params) as T["get"];
}

const row = (await this.stmt.get(...params)) as Record<string, unknown>;
if (!row) return undefined as T["get"];

if (this.customResultMapper) {
const arr = rowToArray(this.fields, row);
return this.customResultMapper([arr]) as T["get"];
}

return mapRow(this.fields!, row) as T["get"];
}

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);
}

/** @internal */
isResponseInArrayMode(): boolean {
// db0 always returns object rows, never array rows
return false;
return this.isResponseInArrayMode_;
}
}
Loading