Spaces:
Running
Running
File size: 6,228 Bytes
aa89cb5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | /** Render provider placeholders as SQL literals so the statement can be copied into a SQL client. */
export type SQLDatabaseKind = 'postgresql' | 'mysql' | 'sqlite';
export type SQLParameterLogPolicy = 'plain' | 'masked' | 'credential' | 'unknown';
export function debugSQL(
parameterizedSQL: string,
parameters: readonly unknown[],
databaseKind: SQLDatabaseKind = 'sqlite',
): string {
return renderSQL(parameterizedSQL, parameters, databaseKind);
}
/** Shared scanner for diagnostics. A callback enables strict bind accounting. */
export function renderSQL(
parameterizedSQL: string,
parameters: readonly unknown[],
databaseKind: SQLDatabaseKind,
parameterLiteral?: (index: number) => string,
): string {
if (parameterLiteral && !parameterizedSQL.trim()) throw new Error('Missing SQL template');
let positionalIndex = 0;
const used = new Set<number>();
const literal = (index: number): string => {
if (index < 0 || index >= parameters.length) throw new Error('SQL bind count mismatch');
used.add(index);
return parameterLiteral ? parameterLiteral(index) : sqlLiteral(parameters[index], databaseKind);
};
let result = '';
let state: 'sql' | 'single' | 'double' | 'backtick' | 'line-comment' | 'block-comment' = 'sql';
for (let index = 0; index < parameterizedSQL.length; index++) {
const char = parameterizedSQL[index];
const next = parameterizedSQL[index + 1] ?? '';
if (state === 'sql' && char === "'") {
result += char;
state = 'single';
continue;
}
if (state === 'sql' && char === '"') { result += char; state = 'double'; continue; }
if (state === 'sql' && char === '`') { result += char; state = 'backtick'; continue; }
if (state === 'sql' && char === '-' && next === '-') {
result += '--'; index++; state = 'line-comment'; continue;
}
if (state === 'sql' && char === '/' && next === '*') {
result += '/*'; index++; state = 'block-comment'; continue;
}
if (state === 'single') {
result += char;
if (char === "'" && next === "'") result += parameterizedSQL[++index];
else if (char === "'") state = 'sql';
continue;
}
if (state === 'double') {
result += char;
if (char === '"' && next === '"') result += parameterizedSQL[++index];
else if (char === '"') state = 'sql';
continue;
}
if (state === 'backtick') {
result += char;
if (char === '`' && next === '`') result += parameterizedSQL[++index];
else if (char === '`') state = 'sql';
continue;
}
if (state === 'line-comment') {
result += char;
if (char === '\r' || char === '\n') state = 'sql';
continue;
}
if (state === 'block-comment') {
result += char;
if (char === '*' && next === '/') { result += '/'; index++; state = 'sql'; }
continue;
}
if (char === '?') {
if (parameterLiteral || positionalIndex < parameters.length) result += literal(positionalIndex++);
else result += char;
continue;
}
if (char === '$' && /[0-9]/.test(parameterizedSQL[index + 1] ?? '')) {
let end = index + 1;
while (/[0-9]/.test(parameterizedSQL[end] ?? '')) end++;
const parameterIndex = Number(parameterizedSQL.slice(index + 1, end)) - 1;
result += parameterLiteral || (parameterIndex >= 0 && parameterIndex < parameters.length)
? literal(parameterIndex) : parameterizedSQL.slice(index, end);
index = end - 1;
continue;
}
if (parameterizedSQL.slice(index).match(/^@p[0-9]+/i)) {
const placeholder = parameterizedSQL.slice(index).match(/^@p([0-9]+)/i)!;
const parameterIndex = Number(placeholder[1]) - 1;
result += parameterLiteral || (parameterIndex >= 0 && parameterIndex < parameters.length)
? literal(parameterIndex) : placeholder[0];
index += placeholder[0].length - 1;
continue;
}
result += char;
}
if (parameterLiteral && (used.size !== parameters.length || (state !== 'sql' && state !== 'line-comment'))) {
throw new Error('Incomplete SQL diagnostic rendering');
}
return result;
}
export function sqlLiteral(value: unknown, databaseKind: SQLDatabaseKind): string {
if (value && typeof value === 'object' && 'type' in value) {
const typed = value as { type: string; value?: unknown };
if (typed.type === 'Null' || typed.type === 'TypedNull') return 'NULL';
if (typed.type === 'Date') {
const date = typed.value instanceof Date
? typed.value.toISOString().slice(0, 10) : String(typed.value);
if (databaseKind === 'postgresql') return `DATE ${quoteSQLString(date)}`;
if (databaseKind === 'mysql') return `CAST(${quoteSQLString(date)} AS DATE)`;
return quoteSQLString(date);
}
if (typed.type === 'Timestamp') {
if (databaseKind === 'sqlite') return String(typed.value);
const iso = new Date(Number(typed.value)).toISOString();
if (databaseKind === 'postgresql') return `TIMESTAMPTZ ${quoteSQLString(iso)}`;
return `CAST(${quoteSQLString(iso.slice(0, 23).replace('T', ' '))} AS DATETIME(3))`;
}
if (typed.type === 'Bool') return typed.value ? 'TRUE' : 'FALSE';
if (['I64', 'U64', 'F64', 'Decimal'].includes(typed.type)) return String(typed.value);
if (typed.type === 'Text') return quoteSQLString(String(typed.value));
}
if (value === null || value === undefined) return 'NULL';
if (typeof value === 'boolean') return value ? 'TRUE' : 'FALSE';
if (typeof value === 'number' || typeof value === 'bigint') return String(value);
if (value instanceof Date) {
if (databaseKind === 'sqlite') return String(value.getTime());
if (databaseKind === 'postgresql') return `TIMESTAMPTZ ${quoteSQLString(value.toISOString())}`;
return `CAST(${quoteSQLString(value.toISOString().slice(0, 23).replace('T', ' '))} AS DATETIME(3))`;
}
if (value instanceof Uint8Array) {
return `X'${Array.from(value, byte => byte.toString(16).padStart(2, '0')).join('')}'`;
}
if (typeof value === 'object') return quoteSQLString(JSON.stringify(value));
return quoteSQLString(String(value));
}
function quoteSQLString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}
|