Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

sql-escaper

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sql-escaper - npm Package Compare versions

Comparing version
1.0.0
to
1.1.0
+152
-31
lib/index.js

@@ -10,3 +10,2 @@ "use strict";

const regex = {
set: /\bSET\b|\bKEY\s+UPDATE\b/i,
backtick: /`/g,

@@ -28,8 +27,101 @@ dot: /\./g,

};
const isRecord = (val) => typeof val === 'object' && val !== null && !Array.isArray(val);
const isDate = (val) => Object.prototype.toString.call(val) === '[object Date]';
const hasSqlString = (val) => typeof val === 'object' &&
val !== null &&
'toSqlString' in val &&
typeof val.toSqlString === 'function';
const charCode = {
singleQuote: 39,
backslash: 92,
dash: 45,
slash: 47,
asterisk: 42,
questionMark: 63,
newline: 10,
space: 32,
tab: 9,
carriageReturn: 13,
};
const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
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) => {
for (let offset = 0; offset < word.length; offset++)
if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
return false;
return ((position === 0 || !isWordChar(sql.charCodeAt(position - 1))) &&
(position + word.length >= length ||
!isWordChar(sql.charCodeAt(position + word.length))));
};
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.dash && nextChar === charCode.dash) {
const lineBreak = sql.indexOf('\n', position + 2);
return lineBreak === -1 ? sql.length : lineBreak + 1;
}
if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
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 === charCode.singleQuote ||
code === charCode.dash ||
code === charCode.slash) {
const contextEnd = skipSqlContext(sql, position);
if (contextEnd !== -1)
position = contextEnd - 1;
}
}
return -1;
};
const findSetKeyword = (sql) => {
const length = sql.length;
for (let position = 0; position < length; position++) {
const code = sql.charCodeAt(position);
const lower = code | 32;
if (code === charCode.singleQuote ||
code === charCode.dash ||
code === charCode.slash) {
const contextEnd = skipSqlContext(sql, position);
if (contextEnd !== -1) {
position = contextEnd - 1;
continue;
}
}
if (lower === 115 && matchesWord(sql, position, 'set', length))
return position;
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 position;
}
}
return -1;
};
const isDate = (value) => Object.prototype.toString.call(value) === '[object Date]';
const hasSqlString = (value) => typeof value === 'object' &&
value !== null &&
'toSqlString' in value &&
typeof value.toSqlString === 'function';
const escapeString = (value) => {

@@ -41,4 +133,4 @@ regex.escapeChars.lastIndex = 0;

for (match = regex.escapeChars.exec(value); match !== null; match = regex.escapeChars.exec(value)) {
escapedValue +=
value.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
escapedValue += value.slice(chunkIndex, match.index);
escapedValue += CHARS_ESCAPE_MAP[match[0]];
chunkIndex = regex.escapeChars.lastIndex;

@@ -52,3 +144,11 @@ }

};
const zeroPad = (number, length) => String(number).padStart(length, '0');
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) => {

@@ -66,4 +166,3 @@ if (tz === 'Z')

const dateToString = (date, timezone) => {
const adjustedDate = new Date(date);
if (Number.isNaN(adjustedDate.getTime()))
if (Number.isNaN(date.getTime()))
return 'NULL';

@@ -78,15 +177,16 @@ let year;

if (timezone === 'local') {
year = adjustedDate.getFullYear();
month = adjustedDate.getMonth() + 1;
day = adjustedDate.getDate();
hour = adjustedDate.getHours();
minute = adjustedDate.getMinutes();
second = adjustedDate.getSeconds();
millisecond = adjustedDate.getMilliseconds();
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);
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0) {
adjustedDate.setTime(adjustedDate.getTime() + timezoneOffsetMinutes * 60000);
}
let time = date.getTime();
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
time += timezoneOffsetMinutes * 60000;
const adjustedDate = new Date(time);
year = adjustedDate.getUTCFullYear();

@@ -101,4 +201,15 @@ month = adjustedDate.getUTCMonth() + 1;

// YYYY-MM-DD HH:mm:ss.mmm
const formattedDateTime = `${zeroPad(year, 4)}-${zeroPad(month, 2)}-${zeroPad(day, 2)} ${zeroPad(hour, 2)}:${zeroPad(minute, 2)}:${zeroPad(second, 2)}.${zeroPad(millisecond, 3)}`;
return escapeString(formattedDateTime);
return escapeString(pad4(year) +
'-' +
pad2(month) +
'-' +
pad2(day) +
' ' +
pad2(hour) +
':' +
pad2(minute) +
':' +
pad2(second) +
'.' +
pad3(millisecond));
};

@@ -128,8 +239,17 @@ exports.dateToString = dateToString;

const objectToValues = (object, timezone) => {
const keys = Object.keys(object);
const keysLength = keys.length;
if (keysLength === 0)
return '';
let sql = '';
for (const key in object) {
for (let i = 0; i < keysLength; i++) {
const key = keys[i];
const value = object[key];
if (typeof value === 'function')
continue;
sql += `${(sql.length === 0 ? '' : ', ') + (0, exports.escapeId)(key)} = ${(0, exports.escape)(value, true, timezone)}`;
if (sql.length > 0)
sql += ', ';
sql += (0, exports.escapeId)(key);
sql += ' = ';
sql += (0, exports.escape)(value, true, timezone);
}

@@ -193,3 +313,3 @@ return sql;

let valuesIndex = 0;
let placeholderPosition = sql.indexOf('?');
let placeholderPosition = findNextPlaceholder(sql, 0);
while (valuesIndex < length && placeholderPosition !== -1) {

@@ -204,3 +324,3 @@ // Count consecutive question marks to detect ? vs ?? vs ???+

if (placeholderLength > 2) {
placeholderPosition = sql.indexOf('?', placeholderEnd);
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
continue;

@@ -217,3 +337,3 @@ }

if (setIndex === -2)
setIndex = sql.search(regex.set);
setIndex = findSetKeyword(sql);
if (setIndex !== -1 &&

@@ -232,6 +352,7 @@ setIndex < placeholderPosition &&

escapedValue = (0, exports.escape)(currentValue, stringifyObjects, timezone);
result += sql.slice(chunkIndex, placeholderPosition) + escapedValue;
result += sql.slice(chunkIndex, placeholderPosition);
result += escapedValue;
chunkIndex = placeholderEnd;
valuesIndex++;
placeholderPosition = sql.indexOf('?', placeholderEnd);
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
}

@@ -238,0 +359,0 @@ if (chunkIndex === 0)

+114
-39
import { Buffer } from "node:buffer";
const regex = {
set: /\bSET\b|\bKEY\s+UPDATE\b/i,
backtick: /`/g,

@@ -20,5 +19,81 @@ dot: /\./g,

};
const isRecord = (val) => typeof val === "object" && val !== null && !Array.isArray(val);
const isDate = (val) => Object.prototype.toString.call(val) === "[object Date]";
const hasSqlString = (val) => typeof val === "object" && val !== null && "toSqlString" in val && typeof val.toSqlString === "function";
const charCode = {
singleQuote: 39,
backslash: 92,
dash: 45,
slash: 47,
asterisk: 42,
questionMark: 63,
newline: 10,
space: 32,
tab: 9,
carriageReturn: 13
};
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
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) => {
for (let offset = 0; offset < word.length; offset++)
if (toLower(sql.charCodeAt(position + offset)) !== word.charCodeAt(offset))
return false;
return (position === 0 || !isWordChar(sql.charCodeAt(position - 1))) && (position + word.length >= length || !isWordChar(sql.charCodeAt(position + word.length)));
};
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.dash && nextChar === charCode.dash) {
const lineBreak = sql.indexOf("\n", position + 2);
return lineBreak === -1 ? sql.length : lineBreak + 1;
}
if (currentChar === charCode.slash && nextChar === charCode.asterisk) {
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 === charCode.singleQuote || code === charCode.dash || code === charCode.slash) {
const contextEnd = skipSqlContext(sql, position);
if (contextEnd !== -1) position = contextEnd - 1;
}
}
return -1;
};
const findSetKeyword = (sql) => {
const length = sql.length;
for (let position = 0; position < length; position++) {
const code = sql.charCodeAt(position);
const lower = code | 32;
if (code === charCode.singleQuote || code === charCode.dash || code === charCode.slash) {
const contextEnd = skipSqlContext(sql, position);
if (contextEnd !== -1) {
position = contextEnd - 1;
continue;
}
}
if (lower === 115 && matchesWord(sql, position, "set", length))
return position;
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 position;
}
}
return -1;
};
const isDate = (value) => Object.prototype.toString.call(value) === "[object Date]";
const hasSqlString = (value) => typeof value === "object" && value !== null && "toSqlString" in value && typeof value.toSqlString === "function";
const escapeString = (value) => {

@@ -30,3 +105,4 @@ regex.escapeChars.lastIndex = 0;

for (match = regex.escapeChars.exec(value); match !== null; match = regex.escapeChars.exec(value)) {
escapedValue += value.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]];
escapedValue += value.slice(chunkIndex, match.index);
escapedValue += CHARS_ESCAPE_MAP[match[0]];
chunkIndex = regex.escapeChars.lastIndex;

@@ -39,3 +115,5 @@ }

};
const zeroPad = (number, length) => String(number).padStart(length, "0");
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) => {

@@ -49,4 +127,3 @@ if (tz === "Z") return 0;

const dateToString = (date, timezone) => {
const adjustedDate = new Date(date);
if (Number.isNaN(adjustedDate.getTime())) return "NULL";
if (Number.isNaN(date.getTime())) return "NULL";
let year;

@@ -60,16 +137,15 @@ let month;

if (timezone === "local") {
year = adjustedDate.getFullYear();
month = adjustedDate.getMonth() + 1;
day = adjustedDate.getDate();
hour = adjustedDate.getHours();
minute = adjustedDate.getMinutes();
second = adjustedDate.getSeconds();
millisecond = adjustedDate.getMilliseconds();
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);
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0) {
adjustedDate.setTime(
adjustedDate.getTime() + timezoneOffsetMinutes * 6e4
);
}
let time = date.getTime();
if (timezoneOffsetMinutes !== false && timezoneOffsetMinutes !== 0)
time += timezoneOffsetMinutes * 6e4;
const adjustedDate = new Date(time);
year = adjustedDate.getUTCFullYear();

@@ -83,10 +159,5 @@ month = adjustedDate.getUTCMonth() + 1;

}
const formattedDateTime = `${zeroPad(year, 4)}-${zeroPad(month, 2)}-${zeroPad(
day,
2
)} ${zeroPad(hour, 2)}:${zeroPad(minute, 2)}:${zeroPad(second, 2)}.${zeroPad(
millisecond,
3
)}`;
return escapeString(formattedDateTime);
return escapeString(
pad4(year) + "-" + pad2(month) + "-" + pad2(day) + " " + pad2(hour) + ":" + pad2(minute) + ":" + pad2(second) + "." + pad3(millisecond)
);
};

@@ -111,11 +182,14 @@ const escapeId = (value, forbidQualified) => {

const objectToValues = (object, timezone) => {
const keys = Object.keys(object);
const keysLength = keys.length;
if (keysLength === 0) return "";
let sql = "";
for (const key in object) {
for (let i = 0; i < keysLength; i++) {
const key = keys[i];
const value = object[key];
if (typeof value === "function") continue;
sql += `${(sql.length === 0 ? "" : ", ") + escapeId(key)} = ${escape(
value,
true,
timezone
)}`;
if (sql.length > 0) sql += ", ";
sql += escapeId(key);
sql += " = ";
sql += escape(value, true, timezone);
}

@@ -166,3 +240,3 @@ return sql;

let valuesIndex = 0;
let placeholderPosition = sql.indexOf("?");
let placeholderPosition = findNextPlaceholder(sql, 0);
while (valuesIndex < length && placeholderPosition !== -1) {

@@ -175,3 +249,3 @@ let placeholderEnd = placeholderPosition + 1;

if (placeholderLength > 2) {
placeholderPosition = sql.indexOf("?", placeholderEnd);
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
continue;

@@ -182,3 +256,3 @@ }

else if (typeof currentValue === "object" && currentValue !== null && !stringifyObjects) {
if (setIndex === -2) setIndex = sql.search(regex.set);
if (setIndex === -2) setIndex = findSetKeyword(sql);
if (setIndex !== -1 && setIndex < placeholderPosition && !hasSqlString(currentValue) && !Array.isArray(currentValue) && !Buffer.isBuffer(currentValue) && !isDate(currentValue) && isRecord(currentValue))

@@ -188,6 +262,7 @@ escapedValue = objectToValues(currentValue, timezone);

} else escapedValue = escape(currentValue, stringifyObjects, timezone);
result += sql.slice(chunkIndex, placeholderPosition) + escapedValue;
result += sql.slice(chunkIndex, placeholderPosition);
result += escapedValue;
chunkIndex = placeholderEnd;
valuesIndex++;
placeholderPosition = sql.indexOf("?", placeholderEnd);
placeholderPosition = findNextPlaceholder(sql, placeholderEnd);
}

@@ -194,0 +269,0 @@ if (chunkIndex === 0) return sql;

{
"name": "sql-escaper",
"version": "1.0.0",
"version": "1.1.0",
"description": "🛡️ Faster SQL escape and format for JavaScript (Node.js, Bun, and Deno).",

@@ -5,0 +5,0 @@ "main": "./lib/index.js",

@@ -317,8 +317,8 @@ <h1 align="center">SQL Escaper</h1>

| ---------------------------------------- | ---------: | ----------: | ---------------: |
| Select 100 values | 460.9 ms | 242.2 ms | **1.90x faster** |
| Insert 100 values | 468.6 ms | 242.5 ms | **1.93x faster** |
| SET with 100 values | 484.2 ms | 257.0 ms | **1.88x faster** |
| SET with 100 objects | 671.6 ms | 283.2 ms | **2.37x faster** |
| ON DUPLICATE KEY UPDATE with 100 values | 894.0 ms | 459.8 ms | **1.94x faster** |
| ON DUPLICATE KEY UPDATE with 100 objects | 1,092.0 ms | 485.7 ms | **2.25x faster** |
| Select 100 values | 450.3 ms | 178.1 ms | **2.53x faster** |
| Insert 100 values | 453.2 ms | 198.0 ms | **2.29x faster** |
| SET with 100 values | 471.3 ms | 213.8 ms | **2.20x faster** |
| SET with 100 objects | 656.9 ms | 249.6 ms | **2.63x faster** |
| ON DUPLICATE KEY UPDATE with 100 values | 873.2 ms | 397.8 ms | **2.20x faster** |
| ON DUPLICATE KEY UPDATE with 100 objects | 1,076.0 ms | 442.6 ms | **2.43x faster** |

@@ -325,0 +325,0 @@ - See detailed results and how the benchmarks are run in the [**benchmark**](https://github.com/wellwelwel/sql-escaper/tree/main/benchmark) directory.