Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ const charCode = {
} as const;

const isRecord = (value: unknown): value is Record<string, SqlValue> =>
typeof value === 'object' && value !== null && !Array.isArray(value);
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Set) &&
!(value instanceof Map);

const isWordChar = (code: number): boolean =>
(code >= 65 && code <= 90) ||
Expand Down Expand Up @@ -349,19 +353,22 @@ export const escapeId = (
};

export const objectToValues = (
object: Record<string, SqlValue>,
object: Record<string, SqlValue> | Map<string, SqlValue>,
timezone?: Timezone
): string => {
const keys = Object.keys(object);
const keysLength = keys.length;
const entries: [string, SqlValue][] =
object instanceof Map
? Array.from(object.entries(), ([k, v]) => [String(k), v])
: Object.entries(object);

const keysLength = entries.length;

if (keysLength === 0) return '';

let sql = '';

for (let i = 0; i < keysLength; i++) {
const key = keys[i]!;
const value = object[key];
const [key, value] = entries[i]!;

if (typeof value === 'function') continue;

Expand All @@ -384,8 +391,12 @@ export const arrayToList = (array: SqlValue[], timezone?: Timezone): string => {
for (let i = 0; i < length; i++) {
const value = array[i];

if (Array.isArray(value)) parts[i] = `(${arrayToList(value, timezone)})`;
else parts[i] = escape(value, true, timezone);
if (Array.isArray(value) || value instanceof Set) {
const sub = Array.isArray(value)
? value
: Array.from(value as Set<SqlValue>);
parts[i] = `(${arrayToList(sub, timezone)})`;
} else parts[i] = escape(value, true, timezone);
}

return parts.join(', ');
Expand All @@ -410,13 +421,16 @@ export const escape = (
case 'object': {
if (isDate(value)) return dateToString(value, timezone || 'local');
if (Array.isArray(value)) return arrayToList(value, timezone);
if (value instanceof Set)
return arrayToList(Array.from(value as Set<SqlValue>), 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 === undefined || stringifyObjects === null))
return escapeString(String(value));
if (isRecord(value)) return objectToValues(value, timezone);
if (isRecord(value) || value instanceof Map)
return objectToValues(value, timezone);

return escapeString(String(value));
}
Expand Down Expand Up @@ -480,7 +494,7 @@ export const format = (
!Buffer.isBuffer(currentValue) &&
!(currentValue instanceof Uint8Array) &&
!isDate(currentValue) &&
isRecord(currentValue)
(isRecord(currentValue) || currentValue instanceof Map)
) {
escapedValue = objectToValues(currentValue, timezone);
setIndex = findSetKeyword(sql, placeholderEnd);
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type SqlValue =
| Raw
| Record<string, unknown>
| SqlValue[]
| Set<SqlValue>
| Map<string, SqlValue>
| null
| undefined;

Expand Down
54 changes: 54 additions & 0 deletions test/everyday-queries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,3 +616,57 @@ describe('Critical: Backtick-quoted identifiers with comment-like sequences', ()
assert.equal(sql, 'SELECT `id`, `name` FROM users WHERE id = 1');
});
});

describe('Set (treated like Array) and Map (treated like Object) via ?', () => {
test('WHERE id IN (?) with Set of numbers', () => {
const sql = format('SELECT * FROM users WHERE id IN (?)', [
new Set([1, 2, 3]),
]);
assert.equal(sql, 'SELECT * FROM users WHERE id IN (1, 2, 3)');
});

test('WHERE status IN (?) with Set of strings', () => {
const sql = format('SELECT * FROM users WHERE status IN (?)', [
new Set(['active', 'pending']),
]);
assert.equal(
sql,
"SELECT * FROM users WHERE status IN ('active', 'pending')"
);
});

test('SELECT ? with Set expands like array', () => {
const sql = format('SELECT ?', [new Set([42])]);
assert.equal(sql, 'SELECT 42');
});

test('UPDATE t SET ? with Map expands like object', () => {
const sql = format('UPDATE t SET ?', [
new Map<string, number | string>([
['name', 'foo'],
['count', 7],
]),
]);
assert.equal(sql, "UPDATE t SET `name` = 'foo', `count` = 7");
});

test('Map in regular ? position stringifies like object', () => {
const sql = format('SELECT * FROM t WHERE data = ?', [new Map([['x', 1]])]);
assert.equal(sql, "SELECT * FROM t WHERE data = '[object Map]'");
});

test('Set in SET position treated like array (lists)', () => {
const sql = format('UPDATE t SET ?', [new Set(['a', 'b'])]);
assert.equal(sql, "UPDATE t SET 'a', 'b'");
});

test('empty Set in IN (?)', () => {
const sql = format('SELECT * FROM t WHERE id IN (?)', [new Set()]);
assert.equal(sql, 'SELECT * FROM t WHERE id IN ()');
});

test('empty Map in SET ?', () => {
const sql = format('UPDATE t SET ?', [new Map()]);
assert.equal(sql, 'UPDATE t SET ');
});
});
Loading