Erster Commit
This commit is contained in:
+16
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
|
||||
* MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
|
||||
*/
|
||||
import type { Raw, SqlValue, TemporalValue, Timezone } from './types.js';
|
||||
import { Buffer } from 'node:buffer';
|
||||
export type { Raw, SqlValue, TemporalValue, Timezone } from './types.js';
|
||||
export declare const dateToString: (date: Date, timezone: Timezone) => string;
|
||||
export declare const temporalToString: (value: TemporalValue, timezone?: Timezone) => string;
|
||||
export declare const escapeId: (value: SqlValue, forbidQualified?: boolean) => string;
|
||||
export declare const objectToValues: (object: Record<string, SqlValue> | Map<string, SqlValue>, timezone?: Timezone) => string;
|
||||
export declare const bufferToString: (buffer: Buffer) => string;
|
||||
export declare const arrayToList: (array: SqlValue[], timezone?: Timezone) => string;
|
||||
export declare const escape: (value: SqlValue, stringifyObjects?: boolean, timezone?: Timezone) => string;
|
||||
export declare const format: (sql: string, values?: SqlValue | SqlValue[], stringifyObjects?: boolean, timezone?: Timezone) => string;
|
||||
export declare const raw: (sql: string) => Raw;
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
"use strict";
|
||||
/**
|
||||
* Adapted from https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/lib/SqlString.js
|
||||
* MIT LICENSE: https://github.com/mysqljs/sqlstring/blob/cd528556b4b6bcf300c3db515026935dedf7cfa1/LICENSE
|
||||
*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.raw = exports.format = exports.escape = exports.arrayToList = exports.bufferToString = exports.objectToValues = exports.escapeId = exports.temporalToString = exports.dateToString = void 0;
|
||||
const node_buffer_1 = require("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,
|
||||
};
|
||||
// Chars that open a string, identifier, or comment
|
||||
CONTEXT_TRIGGER[charCode.singleQuote] = 1;
|
||||
CONTEXT_TRIGGER[charCode.backtick] = 1;
|
||||
CONTEXT_TRIGGER[charCode.dash] = 1;
|
||||
CONTEXT_TRIGGER[charCode.slash] = 1;
|
||||
// Bucket terminators by their first character
|
||||
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 < 1000
|
||||
? '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 * 60000;
|
||||
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();
|
||||
}
|
||||
// YYYY-MM-DD HH:mm:ss.mmm
|
||||
return escapeString(pad4(year) +
|
||||
'-' +
|
||||
pad2(month) +
|
||||
'-' +
|
||||
pad2(day) +
|
||||
' ' +
|
||||
pad2(hour) +
|
||||
':' +
|
||||
pad2(minute) +
|
||||
':' +
|
||||
pad2(second) +
|
||||
'.' +
|
||||
pad3(millisecond));
|
||||
};
|
||||
exports.dateToString = dateToString;
|
||||
const temporalToString = (value, timezone) => {
|
||||
if (typeof value.epochMilliseconds === 'number')
|
||||
return (0, exports.dateToString)(new Date(value.epochMilliseconds), timezone || 'local');
|
||||
if (value[Symbol.toStringTag] === 'Temporal.PlainDateTime')
|
||||
return escapeString(value.toString().replace('T', ' '));
|
||||
return escapeString(value.toString());
|
||||
};
|
||||
exports.temporalToString = temporalToString;
|
||||
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 += (0, exports.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, '`.`')}\``;
|
||||
};
|
||||
exports.escapeId = escapeId;
|
||||
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 += (0, exports.escapeId)(String(key));
|
||||
sql += ' = ';
|
||||
sql += (0, exports.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 += (0, exports.escapeId)(key);
|
||||
sql += ' = ';
|
||||
sql += (0, exports.escape)(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
exports.objectToValues = objectToValues;
|
||||
const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
|
||||
exports.bufferToString = bufferToString;
|
||||
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 += `(${(0, exports.arrayToList)(value, timezone)})`;
|
||||
else if (value instanceof Set)
|
||||
sql += `(${(0, exports.arrayToList)(Array.from(value), timezone)})`;
|
||||
else
|
||||
sql += (0, exports.escape)(value, true, timezone);
|
||||
}
|
||||
return sql;
|
||||
};
|
||||
exports.arrayToList = arrayToList;
|
||||
const escape = (value, stringifyObjects, timezone) => {
|
||||
if (value === undefined || 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 (0, exports.dateToString)(value, timezone || 'local');
|
||||
if (isTemporal(value))
|
||||
return (0, exports.temporalToString)(value, timezone);
|
||||
if (Array.isArray(value))
|
||||
return (0, exports.arrayToList)(value, timezone);
|
||||
if (value instanceof Set)
|
||||
return (0, exports.arrayToList)(Array.from(value), timezone);
|
||||
if (node_buffer_1.Buffer.isBuffer(value))
|
||||
return (0, exports.bufferToString)(value);
|
||||
if (value instanceof Uint8Array)
|
||||
return (0, exports.bufferToString)(node_buffer_1.Buffer.from(value));
|
||||
if (hasSqlString(value))
|
||||
return String(value.toSqlString());
|
||||
if (!(stringifyObjects === undefined || stringifyObjects === null))
|
||||
return escapeString(String(value));
|
||||
if (isRecord(value) || value instanceof Map)
|
||||
return (0, exports.objectToValues)(value, timezone);
|
||||
return escapeString(String(value));
|
||||
}
|
||||
case 'string':
|
||||
return escapeString(value);
|
||||
default:
|
||||
return escapeString(String(value));
|
||||
}
|
||||
};
|
||||
exports.escape = escape;
|
||||
const format = (sql, values, stringifyObjects, timezone) => {
|
||||
if (values === undefined || values === null)
|
||||
return sql;
|
||||
const valuesArray = Array.isArray(values) ? values : [values];
|
||||
const length = valuesArray.length;
|
||||
let setIndex = -2; // -2 = not yet computed, -1 = no SET found
|
||||
let nextSetIndex = -1; // -1 = no SET after setIndex
|
||||
let result = '';
|
||||
let chunkIndex = 0;
|
||||
let valuesIndex = 0;
|
||||
let placeholderPosition = findNextPlaceholder(sql, 0);
|
||||
while (valuesIndex < length && placeholderPosition !== -1) {
|
||||
// Count consecutive question marks to detect ? vs ?? vs ???+
|
||||
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 = (0, exports.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) {
|
||||
// A SET assignment follows the keyword (a letter) or a comma continuing the list
|
||||
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 = (0, exports.escape)(currentValue, true, timezone);
|
||||
else {
|
||||
// Lazy: resolve the first SET and its successor once
|
||||
if (setIndex === -2) {
|
||||
setIndex = findSetKeyword(sql);
|
||||
nextSetIndex = setIndex === -1 ? -1 : findSetKeyword(sql, setIndex);
|
||||
}
|
||||
// Nearest: advance to the SET closest before this placeholder
|
||||
while (nextSetIndex !== -1 && nextSetIndex <= placeholderPosition) {
|
||||
setIndex = nextSetIndex;
|
||||
nextSetIndex = findSetKeyword(sql, nextSetIndex);
|
||||
}
|
||||
if (setIndex !== -1 &&
|
||||
setIndex <= placeholderPosition &&
|
||||
isInSetAssignmentList(sql, setIndex, placeholderPosition))
|
||||
escapedValue = (0, exports.objectToValues)(currentValue, timezone);
|
||||
else
|
||||
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
||||
}
|
||||
}
|
||||
else
|
||||
escapedValue = (0, exports.escape)(currentValue, true, timezone);
|
||||
}
|
||||
else
|
||||
escapedValue = (0, exports.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;
|
||||
};
|
||||
exports.format = format;
|
||||
const raw = (sql) => {
|
||||
if (typeof sql !== 'string')
|
||||
throw new TypeError('argument sql must be a string');
|
||||
return {
|
||||
toSqlString: () => sql,
|
||||
};
|
||||
};
|
||||
exports.raw = raw;
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
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
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export type Raw = {
|
||||
toSqlString(): string;
|
||||
};
|
||||
/** Avoids the global `Temporal` namespace so consumers don't need TS's ESNext.Temporal lib. */
|
||||
export type TemporalValue = {
|
||||
readonly [Symbol.toStringTag]: `Temporal.${string}`;
|
||||
readonly epochMilliseconds?: number;
|
||||
toString(): string;
|
||||
};
|
||||
export type SqlValue = string | number | bigint | boolean | Date | TemporalValue | Buffer | Uint8Array | Raw | Record<string, unknown> | SqlValue[] | Set<SqlValue> | Map<string, SqlValue> | null | undefined;
|
||||
export type Timezone = 'local' | 'Z' | (string & NonNullable<unknown>);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
Reference in New Issue
Block a user