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
13 changes: 11 additions & 2 deletions drizzle-orm/src/sqlite-proxy/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,26 @@ export class SqliteRemoteDatabase<
}
}

/**
* Callback for executing individual SQL queries against the remote SQLite database.
*
* The `rows` field in the return value depends on the method:
* - `'run'`: `rows` is ignored (can be `[]` or `undefined`)
* - `'all'` / `'values'`: `rows` must be an array of result rows
* - `'get'`: `rows` should be `undefined` when no matching row is found;
* returning an empty array `[]` is also handled gracefully
*/
export type AsyncRemoteCallback = (
sql: string,
params: any[],
method: 'run' | 'all' | 'values' | 'get',
) => Promise<{ rows: any[] }>;
) => Promise<{ rows: any[] | undefined }>;

export type AsyncBatchRemoteCallback = (batch: {
sql: string;
params: any[];
method: 'run' | 'all' | 'values' | 'get';
}[]) => Promise<{ rows: any[] }[]>;
}[]) => Promise<{ rows: any[] | undefined }[]>;

export type RemoteCallback = AsyncRemoteCallback;

Expand Down
10 changes: 8 additions & 2 deletions drizzle-orm/src/sqlite-proxy/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ export class SQLiteRemoteSession<
}

override extractRawGetValueFromBatchResult(result: unknown): unknown {
return (result as SqliteRemoteResult).rows![0];
const rows = (result as SqliteRemoteResult).rows;
return rows?.[0];
}

override extractRawValuesValueFromBatchResult(result: unknown): unknown {
Expand Down Expand Up @@ -226,7 +227,12 @@ export class RemotePreparedQuery<T extends PreparedQueryConfig = PreparedQueryCo
return await (client as AsyncRemoteCallback)(query.sql, params, 'get');
});

return this.mapGetResult(clientResult.rows);
// Normalize empty array to undefined for 'get' with no matching row.
// Native SQLite drivers (better-sqlite3, bun:sqlite) return undefined when
// get() finds no row, but proxy implementers often return [] following the
// typed callback signature. Treat both as "no result".
const rows = clientResult.rows;
return this.mapGetResult(Array.isArray(rows) && rows.length === 0 ? undefined : rows);
}

override mapGetResult(rows: unknown, isFromBatch?: boolean): unknown {
Expand Down
46 changes: 46 additions & 0 deletions drizzle-orm/tests/sqlite-proxy-get.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, test } from 'vitest';
import { int, sqliteTable, text } from '~/sqlite-core/index.ts';
import { drizzle } from '~/sqlite-proxy/index.ts';
import type { AsyncRemoteCallback } from '~/sqlite-proxy/driver.ts';

const users = sqliteTable('users', {
id: int('id').primaryKey(),
name: text('name').notNull(),
});

describe('sqlite-proxy get() with no matching row', () => {
test('returns undefined when callback returns { rows: undefined }', async () => {
const callback: AsyncRemoteCallback = async (_sql, _params, _method) => {
return { rows: undefined };
};

const db = drizzle(callback);
const result = await db.select().from(users).get();

expect(result).toBeUndefined();
});

test('returns undefined when callback returns { rows: [] } for get', async () => {
const callback: AsyncRemoteCallback = async (_sql, _params, _method) => {
// Before the fix, returning an empty array for 'get' would cause
// drizzle to return { id: undefined, name: undefined } instead of undefined
return { rows: [] };
};

const db = drizzle(callback);
const result = await db.select().from(users).get();

expect(result).toBeUndefined();
});

test('returns the row when callback returns a valid result for get', async () => {
const callback: AsyncRemoteCallback = async (_sql, _params, _method) => {
return { rows: [1, 'Alice'] };
};

const db = drizzle(callback);
const result = await db.select().from(users).get();

expect(result).toEqual({ id: 1, name: 'Alice' });
});
});