1import type { MigrationDialect } from "./Format.ts";
2
3/** Matches drizzle-kit v1 migration directories: `YYYYMMDDHHMMSS_name`. */
4export const DRIZZLE_DIR_PATTERN = /^\d{14}_.+/;
5
6/**
7 * Parse a 14-digit `YYYYMMDDHHMMSS` prefix into UTC millis (drizzle's
8 * `formatToMillis`). Returns undefined when the name has no such prefix.
9 */
10export const timestampPrefixMillis = (name: string): number | undefined => {
11 const prefix = name.slice(0, 14);
12 if (!/^\d{14}$/.test(prefix)) return undefined;
13 const year = Number.parseInt(prefix.slice(0, 4), 10);
14 const month = Number.parseInt(prefix.slice(4, 6), 10) - 1;
15 const day = Number.parseInt(prefix.slice(6, 8), 10);
16 const hour = Number.parseInt(prefix.slice(8, 10), 10);
17 const minute = Number.parseInt(prefix.slice(10, 12), 10);
18 const second = Number.parseInt(prefix.slice(12, 14), 10);
19 return Date.UTC(year, month, day, hour, minute, second);
20};
21
22/**
23 * Render a parameter as a SQL literal. Only used for Alchemy's own
24 * bookkeeping queries (names, hashes, millis, ISO dates) against executors
25 * without native parameter support (the D1 HTTP API and the local workerd
26 * tunnel).
27 */
28export const sqlLiteral = (value: unknown): string => {
29 if (value === null || value === undefined) return "NULL";
30 if (typeof value === "number" || typeof value === "bigint") {
31 return String(value);
32 }
33 if (typeof value === "boolean") return value ? "1" : "0";
34 if (value instanceof Date) return `'${value.toISOString()}'`;
35 return `'${String(value).replaceAll("'", "''")}'`;
36};
37
38/**
39 * Inline `?` (sqlite/mysql) or `$n` (postgres) placeholders as SQL
40 * literals. String scanning respects quoted spans so literal `?`s inside
41 * strings survive.
42 */
43export const inlineSqlParams = (
44 sql: string,
45 params: ReadonlyArray<unknown>,
46 dialect: MigrationDialect,
47): string => {
48 if (params.length === 0) return sql;
49 if (dialect === "postgres") {
50 return sql.replace(/\$(\d+)/g, (match, n: string) => {
51 const index = Number.parseInt(n, 10) - 1;
52 return index >= 0 && index < params.length
53 ? sqlLiteral(params[index])
54 : match;
55 });
56 }
57 let out = "";
58 let paramIndex = 0;
59 let quote: string | undefined;
60 for (let i = 0; i < sql.length; i++) {
61 const ch = sql[i];
62 if (quote) {
63 out += ch;
64 if (ch === quote) quote = undefined;
65 continue;
66 }
67 if (ch === "'" || ch === '"' || ch === "`") {
68 quote = ch;
69 out += ch;
70 continue;
71 }
72 if (ch === "?" && paramIndex < params.length) {
73 out += sqlLiteral(params[paramIndex++]);
74 continue;
75 }
76 out += ch;
77 }
78 return out;
79};
80
81/** Quote an identifier for the given dialect. */
82export const quoteIdentifier = (
83 identifier: string,
84 dialect: MigrationDialect,
85): string =>
86 dialect === "mysql"
87 ? `\`${identifier.replaceAll("`", "``")}\``
88 : `"${identifier.replaceAll('"', '""')}"`;