Switch to Bun.sql + drizzle-orm v1 beta#265
Conversation
Replace `pg` (node-postgres) with Bun's native SQL client for PostgreSQL connections. This drops the `pg` and `@types/pg` dependencies in favor of Bun's built-in `SQL` class, which uses native C/Zig bindings with the binary wire protocol for better performance. Also upgrades from drizzle-orm 0.45.x to the v1 beta (1.0.0-beta.18): - Migrations use the new v1 folder format (timestamp-prefixed directories) - `drizzle-zod` replaced by built-in `drizzle-orm/zod` - Old `meta/_journal.json` migration journal removed Breaking changes: - `api.db.pool` (pg.Pool) replaced with `api.db.client` (Bun.SQL) - `drizzle-zod` peer dependency removed (use `drizzle-orm/zod`) - Existing migration folders must be upgraded via `drizzle-kit up` Closes #263 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- config.md: Remove pool.min and pool.allowExitOnIdle (not in Bun.sql), update pool tuning table and description - initializers.md: Update example type from Pool to SQL client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of hardcoding zod, drizzle-orm, drizzle-kit, and prettier version strings, read them from the framework's own package.json peerDependencies and devDependencies. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of shipping a hardcoded migration SQL string, the scaffold now runs `bun install` + `drizzle-kit generate` against the scaffolded schema to produce the migration dynamically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
I have been playing around with the file import { $ } from "bun";
import { DefaultLogger, type LogWriter, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/bun-sql";
import { migrate } from "drizzle-orm/bun-sql/migrator";
import fs from "node:fs";
import { unlink } from "node:fs/promises";
import path from "path";
import { SQL } from "bun";
import { api, logger } from "../api";
import { Initializer } from "../classes/Initializer";
import { ErrorType, TypedError } from "../classes/TypedError";
import { config } from "../config";
import { formatConnectionStringForLogging } from "../util/connectionString";
const namespace = "db";
declare module "../classes/API" {
export interface API {
[namespace]: Awaited<ReturnType<DB["initialize"]>>;
}
}
export class DB extends Initializer {
constructor() {
super(namespace);
this.loadPriority = 100;
this.startPriority = 100;
this.stopPriority = 910;
}
async initialize() {
const dbContainer = {} as {
db: ReturnType<typeof drizzle>;
client: InstanceType<typeof SQL>;
};
return Object.assign(
{
generateMigrations: this.generateMigrations,
clearDatabase: this.clearDatabase,
},
dbContainer,
);
}
async start() {
api.db.client = new SQL(config.database.connectionString);
class DrizzleLogger implements LogWriter {
write(message: string) {
logger.debug(message);
}
}
api.db.db = drizzle({
client: api.db.client,
logger: new DefaultLogger({ writer: new DrizzleLogger() }),
});
try {
await api.db.db.execute(sql`SELECT NOW()`);
} catch (e) {
throw new TypedError({
type: ErrorType.SERVER_INITIALIZATION,
message: `Cannot connect to database (${formatConnectionStringForLogging(config.database.connectionString)}): ${e}`,
});
}
if (config.database.autoMigrate) {
try {
const migrationsFolder = path.join(api.rootDir, "drizzle");
const journalPath = path.join(
migrationsFolder,
"meta",
"_journal.json",
);
if (!fs.existsSync(journalPath)) {
fs.mkdirSync(path.dirname(journalPath), { recursive: true });
fs.writeFileSync(journalPath, JSON.stringify({ entries: [] }));
logger.info("created empty drizzle migrations journal");
}
// ✅ MOD 1: Pass object with migrationsFolder property
// `migrate()` from drizzle-orm/bun-sql/migrator expects an object with migrationsFolder property,
// not just a string path --> { migrationsFolder }
await migrate(api.db.db, { migrationsFolder });
logger.info("database migrated successfully");
} catch (e) {
throw new TypedError({
type: ErrorType.SERVER_INITIALIZATION,
message: `Cannot migrate database (${formatConnectionStringForLogging(config.database.connectionString)}): ${e}`,
});
}
}
logger.info(
`database connection established (${formatConnectionStringForLogging(config.database.connectionString)})`,
);
}
async stop() {
if (api.db.db && api.db.client) {
try {
await api.db.client.close();
logger.info("database connection closed");
} catch (e) {
logger.error("error closing database connection", e);
}
}
}
/**
* Generate migrations for the database schema.
*/
async generateMigrations() {
// ✅ MOD 2: Use `defineConfig` from `drizzle-kit` instead of the `Config` type
// - the `url` property isn't recognized in the `DrizzleMigrateConfig` type
// - for Bun SQL, we need a different approach
// - also, see comment at the end of the file
const migrationConfig = {
dialect: "postgresql" as const,
schema: path.join(api.rootDir, "schema", "*"),
driver: "bun" as const, // ✅ Specify Bun driver
dbCredentials: {
url: config.database.connectionString,
},
out: path.join(api.rootDir, "drizzle"),
};
const fileContent = `export default ${JSON.stringify(migrationConfig, null, 2)}`;
const tmpfilePath = path.join(api.rootDir, "drizzle", "config.tmp.ts");
try {
await Bun.write(tmpfilePath, fileContent);
const { exitCode, stdout, stderr } =
await $`bun drizzle-kit generate --config ${tmpfilePath}`;
logger.trace(stdout.toString());
if (exitCode !== 0) {
{
throw new TypedError({
message: `Failed to generate migrations: ${stderr.toString()}`,
type: ErrorType.SERVER_INITIALIZATION,
});
}
}
} finally {
const filePointer = Bun.file(tmpfilePath);
if (await filePointer.exists()) await unlink(tmpfilePath);
}
}
/**
* Erase all the tables in the active database.
*/
async clearDatabase(restartIdentity = true, cascade = true) {
if (Bun.env.NODE_ENV === "production") {
throw new TypedError({
message: "clearDatabase cannot be called in production",
type: ErrorType.SERVER_INITIALIZATION,
});
}
const result = await api.db.db.execute(
sql`SELECT tablename FROM pg_tables WHERE schemaname = CURRENT_SCHEMA`,
);
for (const row of result) {
logger.debug(`truncating table ${row.tablename}`);
await api.db.db.execute(
sql.raw(
`TRUNCATE TABLE "${row.tablename}" ${restartIdentity ? "RESTART IDENTITY" : ""} ${cascade ? "CASCADE" : ""} `,
),
);
}
}
}extra comments// before, we had something like
const migrationConfig: DrizzleMigrateConfig = {
dialect: "postgresql" as const,
schema: path.join("schema", "*"),
dbCredentials: {
url: config.database.connectionString,
},
out: path.join("drizzle"),
};
// after:
const migrationConfig = {
dialect: "postgresql" as const,
schema: path.join(api.rootDir, "schema", "*"),
driver: "bun" as const, // Required for Bun SQL
dbCredentials: {
url: config.database.connectionString,
},
out: path.join(api.rootDir, "drizzle"),
};I, basically,
Also, the I used this file with the project generated by the CLI itself1 and the full integration worked—creating a user, creating a session, getting the user session, destroying the session, ensuring there's no user session—worked. It's not production ready haha, but it highlights some of the stuff one could take note of|pay attention to when implementing the version with Bun's native SQL driver. Anyway. Looking forward to seeing this becoming more and more Bun-native. Cheers! Footnotes
|
|
Thanks for this! Would you be able to make this a PR so we can see the diff and comment line-by-line? |
|
Evan, check this https://github.com/theintjengineer/tie-keryx, if you can. |
Resolve version conflict in package.json (take 0.16.1 from main) and regenerate bun.lockb. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@theintjengineer I think I missing the meta about your PR. What problem are you trying to solve or feature are your adding? I'm missing something... |
|
The changes in #285 will confound this work a bit further |
Use bunx instead of bun to run drizzle-kit in scaffold generator so the binary resolves from the scaffolded project's node_modules. Add 60s timeouts to scaffold/upgrade tests that now run bun install + drizzle-kit generate. Bump drizzle-orm and drizzle-kit from beta.18 to beta.20. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve conflicts: keep bun-sql/drizzle-v1 driver and version pins, adopt main's biome formatter (drop prettier), update transaction middleware and utility from pg PoolClient to Bun.sql ReservedSQL, bump version to 0.22.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
drizzle-orm v1 beta does not include a /zod subpath — the zod integration remains in the separate drizzle-zod package. Restore drizzle-zod as a dependency in example/backend and change imports from drizzle-orm/zod to drizzle-zod. Also fix implicit any on transaction callback parameter by adding an explicit type annotation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test checked pg Pool's totalCount to verify connection release. With Bun.sql there is no pool count API — verify behavior by confirming the success path returns correctly and the error path completes without hanging (which would indicate a leaked reserved connection). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
pg(node-postgres) with Bun's nativeSQLclient (drizzle-orm/bun-sql) for PostgreSQL connections — dropspgand@types/pgdependenciesdrizzle-orm@0.45.xtodrizzle-orm@1.0.0-beta.18drizzle-zodwith built-indrizzle-orm/zod(consolidated in v1)meta/_journal.json)Breaking changes
api.db.pool(pg.Pool) →api.db.client(Bun.SQL)drizzle-zodpeer dep removed → usedrizzle-orm/zoddrizzle-kit up --dialect postgresql --out drizzleto upgrade migration foldersNotable implementation detail
Bun.sql has a bug where rapidly creating and closing many
SQLinstances (e.g., in test suites that start/stop the server per file) causes"Failed query"errors. Worked around this by caching theSQLclient instance and reusing it across start/stop cycles when the connection string hasn't changed.Closes #263
Test plan
bun run ci)🤖 Generated with Claude Code