release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+4 -3
View File
@@ -70,8 +70,9 @@ export function databaseNames(): string[] {
/** Close every configured database and clear the registry. */
export async function closeDatabases(): Promise<void> {
for (const entry of registry.values()) {
if (entry.db) await entry.db.close();
}
const databases = [...registry.values()].flatMap((entry) => (entry.db ? [entry.db] : []));
registry.clear();
const results = await Promise.allSettled(databases.map((db) => db.close()));
const errors = results.flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
if (errors.length) throw new AggregateError(errors, "One or more databases failed to close");
}
+34 -7
View File
@@ -50,12 +50,32 @@ export interface Db {
close(): void | Promise<void>;
}
interface DbLifecycle {
closing: boolean;
active: Set<Promise<unknown>>;
closePromise?: Promise<void>;
}
/** Build a `Db` over a query runner (the driver at top level, or a transaction). */
function dbOver(runner: TxHandle, driver: Driver): Db {
function dbOver(
runner: TxHandle,
driver: Driver,
lifecycle: DbLifecycle,
transactionScope = false,
): Db {
function run<T>(operation: () => Promise<T>): Promise<T> {
if (lifecycle.closing && !transactionScope) {
return Promise.reject(new Error("WRN-DB-CLOSED: database is closing or closed"));
}
const promise = Promise.resolve().then(operation);
lifecycle.active.add(promise);
void promise.finally(() => lifecycle.active.delete(promise)).catch(() => {});
return promise;
}
const db: Db = {
driver,
async all(sql, params = [], model) {
const rows = await runner.query(sql, params);
const rows = await run(() => runner.query(sql, params));
return (model ? rows.map((r) => model.parse(r)) : rows) as never;
},
async one(sql, params = [], model) {
@@ -63,18 +83,25 @@ function dbOver(runner: TxHandle, driver: Driver): Db {
return (rows[0] ?? null) as never;
},
exec(sql, params = []) {
return runner.exec(sql, params);
return run(() => runner.exec(sql, params));
},
async tx(fn) {
// Top level opens a real transaction; inside one, reuse the current tx.
if (runner === driver) return driver.transaction((tx) => fn(dbOver(tx, driver)));
if (runner === driver)
return run(() => driver.transaction((tx) => fn(dbOver(tx, driver, lifecycle, true))));
return fn(db);
},
async createTable(model) {
await runner.exec(createTableSql(model, driver.dialect));
await run(() => runner.exec(createTableSql(model, driver.dialect)));
},
close() {
return driver.close();
if (lifecycle.closePromise) return lifecycle.closePromise;
lifecycle.closing = true;
lifecycle.closePromise = (async () => {
await Promise.allSettled([...lifecycle.active]);
await driver.close();
})();
return lifecycle.closePromise;
},
};
return db;
@@ -82,5 +109,5 @@ function dbOver(runner: TxHandle, driver: Driver): Db {
/** Build a `Db` client from a driver. */
export function createDb(driver: Driver): Db {
return dbOver(driver, driver);
return dbOver(driver, driver, { closing: false, active: new Set() });
}
+274
View File
@@ -0,0 +1,274 @@
import type { Db, ExecResult, Row } from "./driver.ts";
import type { Model } from "./schema.ts";
import type { Dialect } from "./sql.ts";
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
function identifier(value: string): string {
if (!IDENTIFIER.test(value)) throw new TypeError(`Unsafe SQL identifier: ${value}`);
return value;
}
function placeholder(dialect: Dialect, index: number): string {
return dialect === "postgres" ? `$${index}` : "?";
}
function placeholders(dialect: Dialect, count: number, start = 1): string[] {
return Array.from({ length: count }, (_value, index) => placeholder(dialect, start + index));
}
function finiteInteger(value: number | undefined, fallback: number, minimum: number): number {
if (value === undefined) return fallback;
if (!Number.isFinite(value)) throw new RangeError("Expected a finite integer.");
return Math.max(minimum, Math.floor(value));
}
export class RecordNotFoundError extends Error {
constructor(message = "Record not found") {
super(message);
this.name = "RecordNotFoundError";
}
}
export async function firstOrThrow<T>(
db: Db,
sql: string,
params: unknown[] = [],
model?: Model<T>,
message?: string,
): Promise<T> {
const row = await db.one(sql, params, model);
if (row === null) throw new RecordNotFoundError(message);
return row;
}
export async function exists(db: Db, sql: string, params: unknown[] = []): Promise<boolean> {
return (await db.one(sql, params)) !== null;
}
export async function countRows(
db: Db,
table: string,
where = "",
params: unknown[] = [],
): Promise<number> {
const row = await db.one<{ count: number | string }>(
`SELECT COUNT(*) AS count FROM ${identifier(table)}${where ? ` WHERE ${where}` : ""}`,
params,
);
return Number(row?.count ?? 0);
}
export function withTransaction<T>(db: Db, callback: (tx: Db) => Promise<T>): Promise<T> {
return db.tx(callback);
}
/** Conservative default classifier for deadlock/serialization retry errors. */
export function isRetryableTransactionError(error: unknown): boolean {
if (!error || typeof error !== "object") return false;
const value = error as { code?: unknown; errno?: unknown; message?: unknown };
const code = String(value.code ?? value.errno ?? "").toUpperCase();
if (["40001", "40P01", "SQLITE_BUSY", "SQLITE_LOCKED", "1213", "1205"].includes(code)) {
return true;
}
const message = String(value.message ?? "").toLowerCase();
return /deadlock|serialization failure|database is locked|lock wait timeout/.test(message);
}
export async function retryTransaction<T>(
db: Db,
callback: (tx: Db, attempt: number) => Promise<T>,
options: {
attempts?: number;
baseDelayMs?: number;
maxDelayMs?: number;
jitter?: boolean;
shouldRetry?: (error: unknown) => boolean;
} = {},
): Promise<T> {
const attempts = finiteInteger(options.attempts, 3, 1);
const baseDelayMs = finiteInteger(options.baseDelayMs, 25, 0);
const maxDelayMs = finiteInteger(options.maxDelayMs, 1_000, 0);
const shouldRetry = options.shouldRetry ?? isRetryableTransactionError;
let lastError: unknown;
for (let attempt = 1; attempt <= attempts; attempt++) {
try {
return await db.tx((tx) => callback(tx, attempt));
} catch (error) {
lastError = error;
if (attempt >= attempts || !shouldRetry(error)) throw error;
const exponential = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1));
const delay =
options.jitter === false
? exponential
: Math.round(exponential * (0.5 + Math.random() * 0.5));
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}
export function batch<T>(values: readonly T[], size = 100): T[][] {
const chunkSize = finiteInteger(size, 100, 1);
const chunks: T[][] = [];
for (let index = 0; index < values.length; index += chunkSize) {
chunks.push(values.slice(index, index + chunkSize));
}
return chunks;
}
export async function databaseHealth(
db: Db,
): Promise<{ ok: boolean; latencyMs: number; error?: string }> {
const start = performance.now();
try {
await db.one("SELECT 1 AS healthy");
return { ok: true, latencyMs: Math.round((performance.now() - start) * 100) / 100 };
} catch (error) {
return {
ok: false,
latencyMs: Math.round((performance.now() - start) * 100) / 100,
error: error instanceof Error ? error.message : String(error),
};
}
}
export interface RepositoryListOptions<T extends Row> {
limit?: number;
offset?: number;
orderBy?: keyof T & string;
direction?: "asc" | "desc";
}
export interface Repository<T extends Row> {
all(options?: RepositoryListOptions<T>): Promise<T[]>;
find(id: string | number): Promise<T | null>;
require(id: string | number): Promise<T>;
create(values: Partial<T>): Promise<ExecResult>;
update(id: string | number, values: Partial<T>): Promise<ExecResult>;
remove(id: string | number): Promise<ExecResult>;
exists(id: string | number): Promise<boolean>;
count(): Promise<number>;
}
export function createRepository<T extends Row>(
db: Db,
input: {
table: string;
idColumn?: string;
model?: Model<T>;
allowedColumns?: readonly (keyof T & string)[];
maxListLimit?: number;
/** Immutable equality scope (normally tenant_id) applied to every operation. */
scope?: { column: keyof T & string; value: unknown };
},
): Repository<T> {
const table = identifier(input.table);
const idColumn = identifier(input.idColumn ?? "id");
const allowed = input.allowedColumns ? new Set(input.allowedColumns.map(identifier)) : null;
const dialect = db.driver.dialect;
const maxListLimit = finiteInteger(input.maxListLimit, 1_000, 1);
const scopeColumn = input.scope ? identifier(input.scope.column) : null;
const valuesOf = (values: Partial<T>) => {
const entries = Object.entries(values).filter(([key, value]) => {
if (value === undefined || key === idColumn) return false;
identifier(key);
return !allowed || allowed.has(key) || key === scopeColumn;
});
if (!entries.length)
throw new TypeError("Repository write requires at least one allowed column.");
return entries;
};
const allowedReadColumn = (value: string): string => {
const safe = identifier(value);
if (allowed && safe !== idColumn && !allowed.has(safe)) {
throw new TypeError(`Repository column is not allowed: ${safe}`);
}
return safe;
};
return {
all: (options = {}) => {
const clauses: string[] = scopeColumn
? [`WHERE ${scopeColumn} = ${placeholder(dialect, 1)}`]
: [];
const params: unknown[] = scopeColumn ? [input.scope!.value] : [];
if (options.orderBy) {
clauses.push(
`ORDER BY ${allowedReadColumn(options.orderBy)} ${(options.direction ?? "asc").toUpperCase()}`,
);
}
if (options.limit !== undefined) {
const limit = Math.min(maxListLimit, finiteInteger(options.limit, maxListLimit, 1));
params.push(limit);
clauses.push(`LIMIT ${placeholder(dialect, params.length)}`);
}
if (options.offset !== undefined) {
const offset = finiteInteger(options.offset, 0, 0);
if (options.limit === undefined) {
params.push(maxListLimit);
clauses.push(`LIMIT ${placeholder(dialect, params.length)}`);
}
params.push(offset);
clauses.push(`OFFSET ${placeholder(dialect, params.length)}`);
}
return db.all<T>(
`SELECT * FROM ${table}${clauses.length ? ` ${clauses.join(" ")}` : ""}`,
params,
input.model,
);
},
find: (id) =>
db.one<T>(
`SELECT * FROM ${table} WHERE ${idColumn} = ${placeholder(dialect, 1)}${scopeColumn ? ` AND ${scopeColumn} = ${placeholder(dialect, 2)}` : ""} LIMIT 1`,
scopeColumn ? [id, input.scope!.value] : [id],
input.model,
),
require: (id) =>
firstOrThrow<T>(
db,
`SELECT * FROM ${table} WHERE ${idColumn} = ${placeholder(dialect, 1)}${scopeColumn ? ` AND ${scopeColumn} = ${placeholder(dialect, 2)}` : ""} LIMIT 1`,
scopeColumn ? [id, input.scope!.value] : [id],
input.model,
),
async create(values) {
const scoped = scopeColumn ? { ...values, [scopeColumn]: input.scope!.value } : values;
const entries = valuesOf(scoped);
return db.exec(
`INSERT INTO ${table} (${entries.map(([key]) => identifier(key)).join(", ")}) VALUES (${placeholders(dialect, entries.length).join(", ")})`,
entries.map(([, value]) => value),
);
},
async update(id, values) {
const entries = valuesOf(values);
return db.exec(
`UPDATE ${table} SET ${entries
.map(([key], index) => `${identifier(key)} = ${placeholder(dialect, index + 1)}`)
.join(
", ",
)} WHERE ${idColumn} = ${placeholder(dialect, entries.length + 1)}${scopeColumn ? ` AND ${scopeColumn} = ${placeholder(dialect, entries.length + 2)}` : ""}`,
[...entries.map(([, value]) => value), id, ...(scopeColumn ? [input.scope!.value] : [])],
);
},
remove: (id) =>
db.exec(
`DELETE FROM ${table} WHERE ${idColumn} = ${placeholder(dialect, 1)}${scopeColumn ? ` AND ${scopeColumn} = ${placeholder(dialect, 2)}` : ""}`,
scopeColumn ? [id, input.scope!.value] : [id],
),
exists: (id) =>
exists(
db,
`SELECT 1 FROM ${table} WHERE ${idColumn} = ${placeholder(dialect, 1)}${scopeColumn ? ` AND ${scopeColumn} = ${placeholder(dialect, 2)}` : ""} LIMIT 1`,
scopeColumn ? [id, input.scope!.value] : [id],
),
count: () =>
scopeColumn
? db
.one<{ count: number | string }>(
`SELECT COUNT(*) AS count FROM ${table} WHERE ${scopeColumn} = ${placeholder(dialect, 1)}`,
[input.scope!.value],
)
.then((row) => Number(row?.count ?? 0))
: countRows(db, table),
};
}
+21 -2
View File
@@ -31,7 +31,9 @@ export {
status,
scaffoldMigration,
} from "./migrate.ts";
export type { Migration } from "./migrate.ts";
export type { Migration, MigrationRunOptions } from "./migrate.ts";
export { analyzeMigrationSafety, analyzeMigrations } from "./migration-safety.ts";
export type { MigrationSafetyIssue } from "./migration-safety.ts";
export { parseQueries, generateQueriesFile } from "./generate.ts";
export type { QueryDef, QueryKind, ModelRef } from "./generate.ts";
export { paginate, loadRelated } from "./query.ts";
@@ -39,5 +41,22 @@ export type { Paginated, PageOptions, RelationOptions } from "./query.ts";
export { cursorPaginate, optimisticUpdate, tenantScope, softDeleteClause } from "./advanced.ts";
export type { CursorPage, CursorPageOptions } from "./advanced.ts";
export { instrumentDb, queryOperation } from "./performance.ts";
export {
instrumentDb,
queryOperation,
getDbPerformanceSnapshot,
resetDbPerformanceSnapshot,
} from "./performance.ts";
export type { QueryIssue, QueryPolicy, QueryRecord } from "./performance.ts";
export {
RecordNotFoundError,
firstOrThrow,
exists,
countRows,
withTransaction,
retryTransaction,
batch,
databaseHealth,
createRepository,
} from "./helpers.ts";
export type { Repository } from "./helpers.ts";
+104 -17
View File
@@ -20,6 +20,18 @@ export interface Migration {
}
const MIGRATIONS_TABLE = "_wire_migrations";
const MIGRATION_LOCKS_TABLE = "_wire_migration_locks";
export interface MigrationRunOptions {
/** Return pending migration names without executing their SQL. */
dryRun?: boolean;
/** Stop safely between migrations. Active database statements cannot be interrupted portably. */
signal?: AbortSignal;
/** Coordinate migration runners through the database. Default true. */
lock?: boolean;
/** Allow recovery of a lock left by a crashed process. Default 5 minutes. */
lockTimeoutMs?: number;
}
/** Split a migration file into its `up` and `down` SQL sections. */
export function parseMigration(name: string, content: string): Migration {
@@ -50,7 +62,7 @@ export function loadMigrations(dir: string): Migration[] {
async function ensureTable(db: Db): Promise<void> {
await db.exec(
`CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (name TEXT PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
`CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (name VARCHAR(255) PRIMARY KEY, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)`,
);
}
@@ -63,38 +75,113 @@ export async function appliedMigrations(db: Db): Promise<string[]> {
return rows.map((r) => r.name);
}
function throwIfAborted(signal?: AbortSignal): void {
if (signal?.aborted) throw signal.reason ?? new DOMException("Migration aborted", "AbortError");
}
async function acquireMigrationLock(db: Db, timeoutMs: number): Promise<() => Promise<void>> {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new RangeError("migration lockTimeoutMs must be a positive number");
}
await db.exec(
`CREATE TABLE IF NOT EXISTS ${MIGRATION_LOCKS_TABLE} (name VARCHAR(255) PRIMARY KEY, owner VARCHAR(255) NOT NULL, expires_at VARCHAR(40) NOT NULL)`,
);
const owner = crypto.randomUUID();
const now = new Date();
await db.exec(`DELETE FROM ${MIGRATION_LOCKS_TABLE} WHERE name = ? AND expires_at <= ?`, [
"global",
now.toISOString(),
]);
try {
await db.exec(
`INSERT INTO ${MIGRATION_LOCKS_TABLE} (name, owner, expires_at) VALUES (?, ?, ?)`,
["global", owner, new Date(now.getTime() + timeoutMs).toISOString()],
);
} catch (error) {
const held = await db.all(`SELECT owner FROM ${MIGRATION_LOCKS_TABLE} WHERE name = ?`, [
"global",
]);
if (held.length === 0) throw error;
throw new Error("WRN-DB-MIGRATION-LOCKED: another process is running database migrations", {
cause: error,
});
}
return async () => {
await db.exec(`DELETE FROM ${MIGRATION_LOCKS_TABLE} WHERE name = ? AND owner = ?`, [
"global",
owner,
]);
};
}
/** Apply an ordered migration list (each in a transaction). Returns applied names. */
export async function applyMigrations(db: Db, migrations: readonly Migration[]): Promise<string[]> {
export async function applyMigrations(
db: Db,
migrations: readonly Migration[],
options: MigrationRunOptions = {},
): Promise<string[]> {
if (migrations.length === 0) return [];
throwIfAborted(options.signal);
const applied = new Set(await appliedMigrations(db));
const pending = migrations.filter((migration) => !applied.has(migration.name));
if (options.dryRun || pending.length === 0) return pending.map(({ name }) => name);
const release =
options.lock === false
? undefined
: await acquireMigrationLock(db, options.lockTimeoutMs ?? 300_000);
const done: string[] = [];
for (const migration of pending) {
await db.tx(async (tx) => {
if (migration.up) await tx.exec(migration.up);
await tx.exec(`INSERT INTO ${MIGRATIONS_TABLE} (name) VALUES (?)`, [migration.name]);
});
done.push(migration.name);
try {
// Re-read after locking because another runner may have completed while we waited.
const current = new Set(await appliedMigrations(db));
for (const migration of pending.filter(({ name }) => !current.has(name))) {
throwIfAborted(options.signal);
await db.tx(async (tx) => {
if (migration.up) await tx.exec(migration.up);
await tx.exec(`INSERT INTO ${MIGRATIONS_TABLE} (name) VALUES (?)`, [migration.name]);
});
done.push(migration.name);
}
return done;
} finally {
await release?.();
}
return done;
}
/** Apply all pending migrations from a directory. */
export async function migrate(db: Db, dir: string): Promise<string[]> {
return applyMigrations(db, loadMigrations(dir));
export async function migrate(
db: Db,
dir: string,
options: MigrationRunOptions = {},
): Promise<string[]> {
return applyMigrations(db, loadMigrations(dir), options);
}
/** Roll back the most recently applied migration. Returns its name, or null. */
export async function rollback(db: Db, dir: string): Promise<string | null> {
export async function rollback(
db: Db,
dir: string,
options: Omit<MigrationRunOptions, "dryRun"> & { dryRun?: boolean } = {},
): Promise<string | null> {
throwIfAborted(options.signal);
const applied = await appliedMigrations(db);
const last = applied[applied.length - 1];
if (!last) return null;
if (options.dryRun) return last;
const release =
options.lock === false
? undefined
: await acquireMigrationLock(db, options.lockTimeoutMs ?? 300_000);
const migration = loadMigrations(dir).find((m) => m.name === last);
await db.tx(async (tx) => {
if (migration?.down) await tx.exec(migration.down);
await tx.exec(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ?`, [last]);
});
return last;
try {
throwIfAborted(options.signal);
await db.tx(async (tx) => {
if (migration?.down) await tx.exec(migration.down);
await tx.exec(`DELETE FROM ${MIGRATIONS_TABLE} WHERE name = ?`, [last]);
});
return last;
} finally {
await release?.();
}
}
/** Full status: every migration file with whether it has been applied. */
+107
View File
@@ -0,0 +1,107 @@
import type { Migration } from "./migrate.ts";
export interface MigrationSafetyIssue {
code:
| "WRN-DB-DROP-TABLE"
| "WRN-DB-DROP-COLUMN"
| "WRN-DB-RENAME"
| "WRN-DB-TYPE-CHANGE"
| "WRN-DB-SET-NOT-NULL"
| "WRN-DB-ADD-REQUIRED"
| "WRN-DB-BLOCKING-INDEX";
severity: "error" | "warning";
migration: string;
statement: string;
recommendation: string;
}
function statements(sql: string): string[] {
return sql
.replace(/\/\*[\s\S]*?\*\//g, " ")
.split(";")
.map((statement) =>
statement
.replace(/--[^\r\n]*/g, " ")
.replace(/\s+/g, " ")
.trim(),
)
.filter(Boolean);
}
export function analyzeMigrationSafety(migration: Migration): MigrationSafetyIssue[] {
const issues: MigrationSafetyIssue[] = [];
const add = (
code: MigrationSafetyIssue["code"],
severity: MigrationSafetyIssue["severity"],
statement: string,
recommendation: string,
): void => {
issues.push({ code, severity, migration: migration.name, statement, recommendation });
};
for (const statement of statements(migration.up)) {
if (/\bDROP\s+TABLE\b/i.test(statement))
add(
"WRN-DB-DROP-TABLE",
"error",
statement,
"Deprecate reads/writes first; drop in a later contract release.",
);
if (
/\bDROP\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*/i.test(statement) &&
/\bALTER\s+TABLE\b/i.test(statement)
)
add(
"WRN-DB-DROP-COLUMN",
"error",
statement,
"Stop all old-version reads before a separate contract migration.",
);
if (/\bRENAME\s+(?:COLUMN\s+)?\b|\bRENAME\s+TO\b/i.test(statement))
add(
"WRN-DB-RENAME",
"error",
statement,
"Add the new name, dual-write/backfill, switch readers, then remove the old name.",
);
if (
/\bALTER\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*\s+(?:TYPE|SET\s+DATA\s+TYPE)\b|\bMODIFY\s+(?:COLUMN\s+)?[A-Za-z_]/i.test(
statement,
)
)
add(
"WRN-DB-TYPE-CHANGE",
"error",
statement,
"Add a compatible column and backfill before switching readers.",
);
if (/\bALTER\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*\s+SET\s+NOT\s+NULL\b/i.test(statement))
add(
"WRN-DB-SET-NOT-NULL",
"error",
statement,
"Backfill and validate existing rows before enforcing NOT NULL.",
);
if (
/\bADD\s+(?:COLUMN\s+)?[A-Za-z_][\w$]*[\s\S]*\bNOT\s+NULL\b/i.test(statement) &&
!/\bDEFAULT\b/i.test(statement)
)
add(
"WRN-DB-ADD-REQUIRED",
"error",
statement,
"Add nullable, backfill in batches, then enforce the constraint.",
);
if (/^CREATE\s+(?:UNIQUE\s+)?INDEX\b/i.test(statement) && !/\bCONCURRENTLY\b/i.test(statement))
add(
"WRN-DB-BLOCKING-INDEX",
"warning",
statement,
"Use an online/concurrent index operation when the database supports it.",
);
}
return issues;
}
export function analyzeMigrations(migrations: readonly Migration[]): MigrationSafetyIssue[] {
return migrations.flatMap(analyzeMigrationSafety);
}
+35 -8
View File
@@ -28,6 +28,24 @@ export interface QueryPolicy {
onIssue?: (issue: QueryIssue) => void | Promise<void>;
}
const recentQueries: QueryRecord[] = [];
const recentQueryIssues: QueryIssue[] = [];
const TELEMETRY_LIMIT = 200;
export function getDbPerformanceSnapshot(): { queries: QueryRecord[]; issues: QueryIssue[] } {
return { queries: structuredClone(recentQueries), issues: structuredClone(recentQueryIssues) };
}
export function resetDbPerformanceSnapshot(): void {
recentQueries.length = 0;
recentQueryIssues.length = 0;
}
function remember<T>(items: T[], value: T): void {
items.push(structuredClone(value));
if (items.length > TELEMETRY_LIMIT) items.splice(0, items.length - TELEMETRY_LIMIT);
}
function normalizedSql(sql: string): string {
return sql
.replace(/--.*$/gm, " ")
@@ -70,12 +88,14 @@ export function instrumentDb(db: Db, policy: QueryPolicy = {}): Db {
const inspect = async (sql: string): Promise<void> => {
const normalized = normalizedSql(sql);
if (policy.warnSelectStar !== false && /^SELECT\s+\*/i.test(normalized)) {
await policy.onIssue?.({
const issue: QueryIssue = {
code: "WRN-DB-SELECT-STAR",
severity: "warning",
message: "Avoid SELECT * in production queries.",
sql,
});
};
remember(recentQueryIssues, issue);
await policy.onIssue?.(issue);
}
if (
policy.warnUnboundedSelect !== false &&
@@ -83,12 +103,14 @@ export function instrumentDb(db: Db, policy: QueryPolicy = {}): Db {
!/\bLIMIT\b/i.test(normalized) &&
!/\bCOUNT\s*\(/i.test(normalized)
) {
await policy.onIssue?.({
const issue: QueryIssue = {
code: "WRN-DB-UNBOUNDED-SELECT",
severity: "warning",
message: "SELECT query has no LIMIT. Prefer cursor pagination for large datasets.",
sql,
});
};
remember(recentQueryIssues, issue);
await policy.onIssue?.(issue);
}
};
@@ -110,22 +132,27 @@ export function instrumentDb(db: Db, policy: QueryPolicy = {}): Db {
operation,
duplicateCount,
};
remember(recentQueries, queryRecord);
await policy.onQuery?.(queryRecord);
if (durationMs >= slowQueryMs) {
await policy.onIssue?.({
const issue: QueryIssue = {
code: "WRN-DB-SLOW-QUERY",
severity: durationMs >= slowQueryMs * 5 ? "error" : "warning",
message: `Query took ${durationMs.toFixed(2)} ms.`,
sql,
});
};
remember(recentQueryIssues, issue);
await policy.onIssue?.(issue);
}
if (duplicateCount === duplicateWarningCount) {
await policy.onIssue?.({
const issue: QueryIssue = {
code: "WRN-DB-DUPLICATE-QUERY",
severity: "warning",
message: `The same query ran ${duplicateCount} times in one request scope (possible N+1).`,
sql,
});
};
remember(recentQueryIssues, issue);
await policy.onIssue?.(issue);
}
};