diff --git a/src/index.ts b/src/index.ts index 3cee1fa..150b04c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -42,7 +42,11 @@ const charCode = { } as const; const isRecord = (value: unknown): value is Record => - 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) || @@ -349,19 +353,22 @@ export const escapeId = ( }; export const objectToValues = ( - object: Record, + object: Record | Map, 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; @@ -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); + parts[i] = `(${arrayToList(sub, timezone)})`; + } else parts[i] = escape(value, true, timezone); } return parts.join(', '); @@ -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), 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)); } @@ -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); diff --git a/src/types.ts b/src/types.ts index f236425..cc69de8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,8 @@ export type SqlValue = | Raw | Record | SqlValue[] + | Set + | Map | null | undefined; diff --git a/test/everyday-queries.test.ts b/test/everyday-queries.test.ts index b7d0356..cc82af7 100644 --- a/test/everyday-queries.test.ts +++ b/test/everyday-queries.test.ts @@ -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([ + ['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 '); + }); +});