import { drizzle } from "drizzle-orm/mysql2"; import type { Pool as MySqlPool } from "mysql2/promise"; import mysql from "mysql2/promise"; import * as relations from "@/db/relations"; import * as schema from "@/db/schema"; import { env } from "@/env"; import { resolveConnectionLimit } from "@/lib/db-pool"; const fullSchema = { ...schema, ...relations }; const globalForDb = globalThis as unknown as { db?: ReturnType; }; function createDb() { const url = new URL(env.DATABASE_URL); const isBuild = process.env.NEXT_PHASE === "phase-production-build" || process.env.NEXT_PHASE === "phase-production-compile"; const connectionLimit = resolveConnectionLimit(env.DATABASE_POOL_SIZE); // Fail fast during SSG so Next can retry/fallback instead of hanging 30s+. const connectTimeout = isBuild ? Math.min(env.DATABASE_CONNECT_TIMEOUT_MS, 5_000) : env.DATABASE_CONNECT_TIMEOUT_MS; const pool = mysql.createPool({ host: url.hostname, port: Number(url.port) || 3306, user: decodeURIComponent(url.username), password: decodeURIComponent(url.password), database: url.pathname.replace(/^\//, ""), charset: "utf8mb4", waitForConnections: true, connectionLimit, queueLimit: 0, connectTimeout, idleTimeout: env.DATABASE_IDLE_TIMEOUT_MS, enableKeepAlive: true, // Allow :placeholder syntax for raw SQL helpers. namedPlaceholders: true, // Reject multiple statements (SQL injection hardening). multipleStatements: false, supportBigNumbers: true, }); return drizzle(pool, { schema: fullSchema, mode: "default", logger: env.NODE_ENV === "development" ? { logQuery(query, params) { console.log("[db]", query, params); }, } : false, }); } export const db = globalForDb.db ?? createDb(); if (env.NODE_ENV !== "production") globalForDb.db = db; export type Db = ReturnType; type PreparedValue = string | number | bigint | boolean | Date | null | Buffer; /** * Server-side prepared statements (mysql2 pool.execute()). * * Drizzle's mysql2 driver runs `.select()` through the text protocol * (pool.query()), so it never uses server-side prepared statements. For * hot-path queries the SQL is parsed once on the server per connection * instead of every execution. Column aliases let you pick exactly the * columns you need (no SELECT *). */ export async function queryPrepared>( sql: string, params?: PreparedValue[], ): Promise { const client = db.$client as MySqlPool; const [rows] = (await client.execute(sql, params)) as unknown as [ T[], unknown, ]; return rows ?? []; } export async function queryPreparedOne>( sql: string, params?: PreparedValue[], ): Promise { const rows = await queryPrepared(sql, params); return rows[0] ?? null; } export * from "@/db/schema";