import { Buffer } from "node:buffer"; const CONTEXT_TRIGGER = new Uint8Array(128); const SET_CLAUSE_TERMINATORS_BY_FIRST = {}; const SET_CLAUSE_TERMINATORS = [ "where", "order", "group", "having", "limit", "union", "returning", "into", "for", "lock", "offset", "window", "procedure", "on" ]; const regex = { backtick: /`/g, dot: /\./g, timezone: /([+\-\s])(\d\d):?(\d\d)?/, escapeChars: /[\0\b\t\n\r\x1a"'\\]/g }; const charCode = { singleQuote: 39, backtick: 96, backslash: 92, dash: 45, slash: 47, asterisk: 42, exclamation: 33, plus: 43, questionMark: 63, comma: 44, openParen: 40, closeParen: 41, semicolon: 59, newline: 10, space: 32, tab: 9, carriageReturn: 13 }; CONTEXT_TRIGGER[charCode.singleQuote] = 1; CONTEXT_TRIGGER[charCode.backtick] = 1; CONTEXT_TRIGGER[charCode.dash] = 1; CONTEXT_TRIGGER[charCode.slash] = 1; for (const word of SET_CLAUSE_TERMINATORS) { const first = word.charCodeAt(0); const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[first]; if (bucket) bucket.push(word); else SET_CLAUSE_TERMINATORS_BY_FIRST[first] = [word]; } const hasOwnProperty = Object.prototype.hasOwnProperty; const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Set) && !(value instanceof Map); const isWordChar = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122 || code >= 48 && code <= 57 || code === 95; const isWhitespace = (code) => code === charCode.space || code === charCode.tab || code === charCode.newline || code === charCode.carriageReturn; const toLower = (code) => code | 32; const matchesWord = (sql, position, word, length) => { const wordLength = word.length; for (let offset = 0; offset < wordLength; offset++) if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset)) return false; return (position === 0 || !isWordChar(sql.charCodeAt(position - 1))) && (position + wordLength >= length || !isWordChar(sql.charCodeAt(position + wordLength))); }; const skipSqlContext = (sql, position) => { const currentChar = sql.charCodeAt(position); const nextChar = sql.charCodeAt(position + 1); if (currentChar === charCode.singleQuote) { for (let cursor = position + 1; cursor < sql.length; cursor++) { if (sql.charCodeAt(cursor) === charCode.backslash) cursor++; else if (sql.charCodeAt(cursor) === charCode.singleQuote) return cursor + 1; } return sql.length; } if (currentChar === charCode.backtick) { const length = sql.length; for (let cursor = position + 1; cursor < length; cursor++) { if (sql.charCodeAt(cursor) !== charCode.backtick) continue; if (sql.charCodeAt(cursor + 1) === charCode.backtick) { cursor++; continue; } return cursor + 1; } return length; } if (currentChar === charCode.dash && nextChar === charCode.dash) { const afterDash = sql.charCodeAt(position + 2); if (Number.isNaN(afterDash) || afterDash <= charCode.space) { const lineBreak = sql.indexOf("\n", position + 2); return lineBreak === -1 ? sql.length : lineBreak + 1; } return -1; } if (currentChar === charCode.slash && nextChar === charCode.asterisk) { const markerChar = sql.charCodeAt(position + 2); if (markerChar === charCode.exclamation || markerChar === charCode.plus) return -1; const commentEnd = sql.indexOf("*/", position + 2); return commentEnd === -1 ? sql.length : commentEnd + 2; } return -1; }; const findNextPlaceholder = (sql, start) => { const sqlLength = sql.length; for (let position = start; position < sqlLength; position++) { const code = sql.charCodeAt(position); if (code === charCode.questionMark) return position; if (code < 128 && CONTEXT_TRIGGER[code]) { const contextEnd = skipSqlContext(sql, position); if (contextEnd !== -1) position = contextEnd - 1; } } return -1; }; const isInSetAssignmentList = (sql, setEnd, placeholderPosition) => { const length = sql.length; let depth = 0; let sawContent = false; let lastWasComma = false; for (let i = setEnd; i < placeholderPosition; ) { const code = sql.charCodeAt(i); if (code < 128 && CONTEXT_TRIGGER[code]) { const contextEnd = skipSqlContext(sql, i); if (contextEnd !== -1) { i = contextEnd; sawContent = true; lastWasComma = false; continue; } } if (isWhitespace(code)) { i++; continue; } if (code === charCode.openParen) { depth++; sawContent = true; lastWasComma = false; i++; continue; } if (code === charCode.closeParen) { if (--depth < 0) return false; sawContent = true; lastWasComma = false; i++; continue; } if (isWordChar(code)) { if (depth === 0 && !(code >= 48 && code <= 57)) { const bucket = SET_CLAUSE_TERMINATORS_BY_FIRST[code | 32]; if (bucket) { for (let t = 0; t < bucket.length; t++) if (matchesWord(sql, i, bucket[t], length)) return false; } } do { i++; } while (i < placeholderPosition && isWordChar(sql.charCodeAt(i))); sawContent = true; lastWasComma = false; continue; } if (depth === 0) { if (code === charCode.semicolon) return false; if (code === charCode.comma) { lastWasComma = true; sawContent = true; i++; continue; } } sawContent = true; lastWasComma = false; i++; } return depth === 0 && (!sawContent || lastWasComma); }; const findSetKeyword = (sql, startFrom = 0) => { const length = sql.length; for (let position = startFrom; position < length; position++) { const code = sql.charCodeAt(position); if (code < 128 && CONTEXT_TRIGGER[code]) { const contextEnd = skipSqlContext(sql, position); if (contextEnd !== -1) { position = contextEnd - 1; continue; } } const lower = code | 32; if (lower === 115 && matchesWord(sql, position, "set", length)) return position + 3; if (lower === 107 && matchesWord(sql, position, "key", length)) { let cursor = position + 3; while (cursor < length && isWhitespace(sql.charCodeAt(cursor))) cursor++; if (matchesWord(sql, cursor, "update", length)) return cursor + 6; } } return -1; }; const isDate = (value) => Object.prototype.toString.call(value) === "[object Date]"; const isTemporal = (value) => Object.prototype.toString.call(value).startsWith("[object Temporal."); const hasSqlString = (value) => typeof value === "object" && value !== null && "toSqlString" in value && typeof value.toSqlString === "function"; const escapeString = (value) => { const escapeChars = regex.escapeChars; escapeChars.lastIndex = 0; const first = escapeChars.exec(value); if (first === null) return `'${value}'`; const length = value.length; let result = "'" + value.slice(0, first.index); let chunkStart = first.index; for (let i = first.index; i < length; i++) { let escaped; switch (value.charCodeAt(i)) { case 0: escaped = "\\0"; break; case 8: escaped = "\\b"; break; case 9: escaped = "\\t"; break; case 10: escaped = "\\n"; break; case 13: escaped = "\\r"; break; case 26: escaped = "\\Z"; break; case 34: escaped = '\\"'; break; case 39: escaped = "\\'"; break; case 92: escaped = "\\\\"; break; default: continue; } result += value.slice(chunkStart, i) + escaped; chunkStart = i + 1; } return result + value.slice(chunkStart) + "'"; }; const pad2 = (value) => value < 10 ? "0" + value : "" + value; const pad3 = (value) => value < 10 ? "00" + value : value < 100 ? "0" + value : "" + value; const pad4 = (value) => value < 10 ? "000" + value : value < 100 ? "00" + value : value < 1e3 ? "0" + value : "" + value; const convertTimezone = (tz) => { if (tz === "Z") return 0; const timezoneMatch = tz.match(regex.timezone); if (timezoneMatch) return (timezoneMatch[1] === "-" ? -1 : 1) * (Number.parseInt(timezoneMatch[2], 10) + (timezoneMatch[3] ? Number.parseInt(timezoneMatch[3], 10) : 0) / 60) * 60; return false; }; const dateToString = (date, timezone) => { if (Number.isNaN(date.getTime())) return "NULL"; let year; let month; let day; let hour; let minute; let second; let millisecond; if (timezone === "local") { year = date.getFullYear(); month = date.getMonth() + 1; day = date.getDate(); hour = date.getHours(); minute = date.getMinutes(); second = date.getSeconds(); millisecond = date.getMilliseconds(); } else { const timezoneOffsetMinutes = convertTimezone(timezone); let time = date.getTime(); if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0) time += timezoneOffsetMinutes * 6e4; const adjustedDate = new Date(time); year = adjustedDate.getUTCFullYear(); month = adjustedDate.getUTCMonth() + 1; day = adjustedDate.getUTCDate(); hour = adjustedDate.getUTCHours(); minute = adjustedDate.getUTCMinutes(); second = adjustedDate.getUTCSeconds(); millisecond = adjustedDate.getUTCMilliseconds(); } return escapeString( pad4(year) + "-" + pad2(month) + "-" + pad2(day) + " " + pad2(hour) + ":" + pad2(minute) + ":" + pad2(second) + "." + pad3(millisecond) ); }; const temporalToString = (value, timezone) => { if (typeof value.epochMilliseconds === "number") return dateToString(new Date(value.epochMilliseconds), timezone || "local"); if (value[Symbol.toStringTag] === "Temporal.PlainDateTime") return escapeString(value.toString().replace("T", " ")); return escapeString(value.toString()); }; const escapeId = (value, forbidQualified) => { if (Array.isArray(value)) { const length = value.length; let sql = ""; for (let i = 0; i < length; i++) { if (i > 0) sql += ", "; sql += escapeId(value[i], forbidQualified); } return sql; } const identifier = String(value); const hasJsonOperator = !forbidQualified && identifier.indexOf("->") !== -1; if (forbidQualified || hasJsonOperator) { if (identifier.indexOf("`") === -1) return `\`${identifier}\``; return `\`${identifier.replace(regex.backtick, "``")}\``; } if (identifier.indexOf("`") === -1 && identifier.indexOf(".") === -1) return `\`${identifier}\``; return `\`${identifier.replace(regex.backtick, "``").replace(regex.dot, "`.`")}\``; }; const objectToValues = (object, timezone) => { let sql = ""; if (object instanceof Map) { for (const [key, value] of object) { if (typeof value === "function") continue; if (sql.length > 0) sql += ", "; sql += escapeId(String(key)); sql += " = "; sql += escape(value, true, timezone); } return sql; } for (const key in object) { if (!hasOwnProperty.call(object, key)) continue; const value = object[key]; if (typeof value === "function") continue; if (sql.length > 0) sql += ", "; sql += escapeId(key); sql += " = "; sql += escape(value, true, timezone); } return sql; }; const bufferToString = (buffer) => `X${escapeString(buffer.toString("hex"))}`; const arrayToList = (array, timezone) => { const length = array.length; let sql = ""; for (let i = 0; i < length; i++) { if (i > 0) sql += ", "; const value = array[i]; if (Array.isArray(value)) sql += `(${arrayToList(value, timezone)})`; else if (value instanceof Set) sql += `(${arrayToList(Array.from(value), timezone)})`; else sql += escape(value, true, timezone); } return sql; }; const escape = (value, stringifyObjects, timezone) => { if (value === void 0 || value === null) return "NULL"; switch (typeof value) { case "boolean": return value ? "true" : "false"; case "number": case "bigint": return value + ""; case "object": { if (isDate(value)) return dateToString(value, timezone || "local"); if (isTemporal(value)) return temporalToString(value, timezone); if (Array.isArray(value)) return arrayToList(value, timezone); if (value instanceof Set) return arrayToList(Array.from(value), timezone); if (Buffer.isBuffer(value)) return bufferToString(value); if (value instanceof Uint8Array) return bufferToString(Buffer.from(value)); if (hasSqlString(value)) return String(value.toSqlString()); if (!(stringifyObjects === void 0 || stringifyObjects === null)) return escapeString(String(value)); if (isRecord(value) || value instanceof Map) return objectToValues(value, timezone); return escapeString(String(value)); } case "string": return escapeString(value); default: return escapeString(String(value)); } }; const format = (sql, values, stringifyObjects, timezone) => { if (values === void 0 || values === null) return sql; const valuesArray = Array.isArray(values) ? values : [values]; const length = valuesArray.length; let setIndex = -2; let nextSetIndex = -1; let result = ""; let chunkIndex = 0; let valuesIndex = 0; let placeholderPosition = findNextPlaceholder(sql, 0); while (valuesIndex < length && placeholderPosition !== -1) { let placeholderEnd = placeholderPosition + 1; let escapedValue; while (sql.charCodeAt(placeholderEnd) === 63) placeholderEnd++; const placeholderLength = placeholderEnd - placeholderPosition; const currentValue = valuesArray[valuesIndex]; if (placeholderLength > 2) { placeholderPosition = findNextPlaceholder(sql, placeholderEnd); continue; } if (placeholderLength === 2) escapedValue = escapeId(currentValue); else if (typeof currentValue === "number" || typeof currentValue === "bigint") escapedValue = `${currentValue}`; else if (typeof currentValue === "object" && currentValue !== null && !stringifyObjects) { const expandable = !(Array.isArray(currentValue) || currentValue instanceof Uint8Array || currentValue instanceof Date || hasSqlString(currentValue) || isDate(currentValue)) && (isRecord(currentValue) || currentValue instanceof Map); if (expandable) { let previous = placeholderPosition - 1; while (previous >= chunkIndex && isWhitespace(sql.charCodeAt(previous))) previous--; const previousChar = previous >= chunkIndex ? toLower(sql.charCodeAt(previous)) : 0; if ((previousChar < 97 || previousChar > 122) && previousChar !== charCode.comma) escapedValue = escape(currentValue, true, timezone); else { if (setIndex === -2) { setIndex = findSetKeyword(sql); nextSetIndex = setIndex === -1 ? -1 : findSetKeyword(sql, setIndex); } while (nextSetIndex !== -1 && nextSetIndex <= placeholderPosition) { setIndex = nextSetIndex; nextSetIndex = findSetKeyword(sql, nextSetIndex); } if (setIndex !== -1 && setIndex <= placeholderPosition && isInSetAssignmentList(sql, setIndex, placeholderPosition)) escapedValue = objectToValues(currentValue, timezone); else escapedValue = escape(currentValue, true, timezone); } } else escapedValue = escape(currentValue, true, timezone); } else escapedValue = escape(currentValue, stringifyObjects, timezone); result += sql.slice(chunkIndex, placeholderPosition); result += escapedValue; chunkIndex = placeholderEnd; valuesIndex++; placeholderPosition = findNextPlaceholder(sql, placeholderEnd); } if (chunkIndex === 0) return sql; if (chunkIndex < sql.length) return result + sql.slice(chunkIndex); return result; }; const raw = (sql) => { if (typeof sql !== "string") throw new TypeError("argument sql must be a string"); return { toSqlString: () => sql }; }; export { arrayToList, bufferToString, dateToString, escape, escapeId, format, objectToValues, raw, temporalToString };