From c9d3bc3678361402456fcdeb9ece3dee7c0a3111 Mon Sep 17 00:00:00 2001 From: karthic Date: Wed, 11 Nov 2026 16:21:07 +0530 Subject: [PATCH] fix(db): export Drizzle's SQL, SQLWrapper, AnyColumn, and BatchItem types from void/db Closes #790 Co-Authored-By: Claude Opus 5.5 --- docs/reference/api/database.md | 17 +++++++ packages/void/src/codegen/db-types.ts | 3 ++ packages/void/src/runtime/db.ts | 2 + packages/void/test/unit/db-types.typecheck.ts | 44 +++++++++++++++++++ .../void/test/unit/plugin-drizzle.test.ts | 14 ++++++ 5 files changed, 80 insertions(+) create mode 100644 packages/void/test/unit/db-types.typecheck.ts diff --git a/docs/reference/api/database.md b/docs/reference/api/database.md index e6a3269aa..5a7899c9a 100644 --- a/docs/reference/api/database.md +++ b/docs/reference/api/database.md @@ -73,6 +73,23 @@ import { db, eq, and, or, desc, like, inArray, sql } from 'void/db'; **Full list:** `sql`, `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `and`, `or`, `not`, `desc`, `asc`, `like`, `ilike`, `notLike`, `inArray`, `notInArray`, `isNull`, `isNotNull`, `between`, `notBetween`, `exists`, `notExists`, `count`, `sum`, `avg`, `min`, `max`. +It also exports the Drizzle types `SQL`, `SQLWrapper`, `AnyColumn`, and `BatchItem` for helpers that take part of a query: + +```ts +import { posts } from '@schema'; +import { db, sql, type BatchItem, type SQL, type SQLWrapper } from 'void/db'; + +function contains(column: SQLWrapper, text: string): SQL { + return sql`instr(${column}, ${text}) > 0`; +} + +// D1 only: `db.batch()` takes at least one query. +const writes: [BatchItem<'sqlite'>, ...Array>] = [ + db.update(posts).set({ published: true }).where(contains(posts.title, 'void')), +]; +await db.batch(writes); +``` + ## Seeding {#seeding} Imported from `"void/seed"`. diff --git a/packages/void/src/codegen/db-types.ts b/packages/void/src/codegen/db-types.ts index cd1006d1c..acc99f7dc 100644 --- a/packages/void/src/codegen/db-types.ts +++ b/packages/void/src/codegen/db-types.ts @@ -5,6 +5,8 @@ import { writeFileIfChanged } from './write-if-changed.ts'; export const DRIZZLE_OPERATORS = 'sql, eq, ne, gt, gte, lt, lte, and, or, not, desc, asc, like, ilike, notLike, inArray, notInArray, isNull, isNotNull, between, notBetween, exists, notExists, count, sum, avg, min, max'; +export const DRIZZLE_TYPES = 'AnyColumn, SQL, SQLWrapper, BatchItem'; + const DIALECT_DB_TYPES = { sqlite: { type: 'DrizzleD1Database', @@ -41,6 +43,7 @@ export function generateDbDts( `export declare const db: ${type}<${schemaType}>;`, `export declare function createDb(${arg}): ${type}<${schemaType}>;`, `export { ${DRIZZLE_OPERATORS} } from "void/_db";`, + `export type { ${DRIZZLE_TYPES} } from "void/_db";`, `export type { ${type} } from "${from}";`, '', ]; diff --git a/packages/void/src/runtime/db.ts b/packages/void/src/runtime/db.ts index 781fc1f77..1eac98cc0 100644 --- a/packages/void/src/runtime/db.ts +++ b/packages/void/src/runtime/db.ts @@ -40,6 +40,8 @@ export { min, max, } from 'drizzle-orm'; +export type { AnyColumn, SQL, SQLWrapper } from 'drizzle-orm'; +export type { BatchItem } from 'drizzle-orm/batch'; export type { DrizzleD1Database } from 'drizzle-orm/d1'; /** diff --git a/packages/void/test/unit/db-types.typecheck.ts b/packages/void/test/unit/db-types.typecheck.ts new file mode 100644 index 000000000..8fc9f36a5 --- /dev/null +++ b/packages/void/test/unit/db-types.typecheck.ts @@ -0,0 +1,44 @@ +import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'; +import { expectTypeOf } from 'vite-plus/test'; +import { + and, + asc, + createDb, + eq, + sql, + type AnyColumn, + type BatchItem, + type SQL, + type SQLWrapper, +} from '../../src/runtime/db.ts'; + +const posts = sqliteTable('posts', { + id: integer('id').primaryKey(), + title: text('title').notNull(), +}); + +declare const binding: D1Database; +const db = createDb(binding); + +function contains(column: SQLWrapper, value: string): SQL { + return sql`instr(${column}, ${value}) > 0`; +} + +function countIf(condition: SQL) { + return sql`sum(case when ${condition} then 1 else 0 end)`; +} + +function orderBy(column: AnyColumn) { + return asc(column); +} + +const condition = and(eq(posts.id, 1), contains(posts.title, 'void')); +expectTypeOf(condition).toEqualTypeOf(); +countIf(eq(posts.id, 1)); +orderBy(posts.title); + +const writes: [BatchItem<'sqlite'>, ...Array>] = [ + db.update(posts).set({ title: 'Void' }).where(eq(posts.id, 1)), +]; +writes.push(db.insert(posts).values({ id: 2, title: 'Void' })); +void db.batch(writes); diff --git a/packages/void/test/unit/plugin-drizzle.test.ts b/packages/void/test/unit/plugin-drizzle.test.ts index 288e2c42c..e16020b51 100644 --- a/packages/void/test/unit/plugin-drizzle.test.ts +++ b/packages/void/test/unit/plugin-drizzle.test.ts @@ -443,6 +443,20 @@ describe('drizzlePlugin db typing generation', () => { ); }); + it.each(['sqlite', 'postgresql', 'mysql'] as const)( + 'exports Drizzle query types from the %s database types', + (dialect) => { + const root = createProjectRoot(); + + drizzlePlugin(root, false, dialect); + + const dts = readFileSync(join(root, '.void', 'db.d.ts'), 'utf8'); + expect(dts).toContain( + 'export type { AnyColumn, SQL, SQLWrapper, BatchItem } from "void/_db";', + ); + }, + ); + it('generates MySQL database types from the dedicated runtime entry', () => { const root = createProjectRoot();