import { describe, expect, test } from "bun:test"; import sqlGenerator from "../src/utils/sql-generator"; const gen = (genObject: any, extra: Record = {}) => sqlGenerator({ tableName: "users", genObject, ...extra }); describe("identifier quoting", () => { test("quotes table and field names", () => { const { string, values } = gen({ query: { email: { value: "a@b.c" } } }); expect(string).toBe("SELECT * FROM `users` WHERE (`email` = ?)"); expect(values).toEqual(["a@b.c"]); }); test("injected field names stay inside the identifier", () => { const { string } = gen({ query: { "1=1 OR id": { value: 1 } } }); expect(string).toBe("SELECT * FROM `users` WHERE (`1=1 OR id` = ?)"); }); test("backticks in identifiers are escaped", () => { const { string } = gen({ query: { "a` OR 1=1 -- ": { value: 1 } } }); expect(string).toContain("`a`` OR 1=1 -- ` = ?"); }); test("qualified names are quoted per part", () => { const { string } = gen({ selectFields: ["p.name"] }); expect(string).toBe("SELECT `p`.`name` FROM `users`"); }); }); describe("input validation", () => { test.each([ ["order strategy", { order: { field: "id", strategy: "DESC; DROP TABLE x" } }], ["limit", { limit: "1; --" }], ["negative offset", { offset: -1 }], ["page", { limit: 10, page: "2 OR 1" }], ["search operator", { query: { a: { value: 1 } }, searchOperator: "OR 1=1" }], ["raw equality", { query: { a: { value: 1, raw_equality: "= 1 OR" } } }], ["join type", { join: [{ tableName: "p", joinType: "CROSS JOIN x;", match: { source: "id", target: "uid" } }] }], ])("rejects invalid %s", (_name, genObject) => { expect(() => gen(genObject)).toThrow(); }); test("rejects non-numeric targetId", () => { expect(() => gen({}, { targetId: "abc" })).toThrow(); }); }); describe("values", () => { test("0 is a real value", () => { expect(gen({ query: { age: { value: 0 } } }).values).toEqual([0]); }); test("0 is kept in arrays", () => { const { string, values } = gen({ query: { id: { value: [0, 1], operator: "OR" } }, }); expect(string).toBe("SELECT * FROM `users` WHERE ((`id` = ? OR `id` = ?))"); expect(values).toEqual([0, 1]); }); test("0 in value objects", () => { const { string, values } = gen({ query: { n: { value: { value: 0, equality: "GREATER THAN" } } }, }); expect(string).toContain("`n` > ?"); expect(values).toEqual([0]); }); test("null does not leave an unbound placeholder", () => { const { string, values } = gen({ query: { email: { value: null } } }); expect(string.split("?").length - 1).toBe(values.length); }); test("LIKE wraps the value", () => { expect(gen({ query: { n: { value: "x", equality: "LIKE" } } }).values).toEqual(["%x%"]); expect(gen({ query: { n: { value: "x", equality: "NOT LIKE" } } }).values).toEqual(["%x%"]); }); test("BETWEEN / NOT BETWEEN", () => { expect(gen({ query: { n: { between: { min: 0, max: 5 } } } }).values).toEqual([0, 5]); const notBetween = gen({ query: { n: { not_between: { min: 1, max: 2 } } } }); expect(notBetween.string).toContain("`n` NOT BETWEEN ? AND ?"); expect(notBetween.values).toEqual([1, 2]); }); }); describe("IN / NOT IN", () => { test("array values become an IN list", () => { const { string, values } = gen({ query: { id: { value: [1, 2, 3], equality: "IN" } } }); expect(string).toBe("SELECT * FROM `users` WHERE (`id` IN (?,?,?))"); expect(values).toEqual([1, 2, 3]); }); test("NOT IN with a single value", () => { const { string, values } = gen({ query: { id: { value: 4, equality: "NOT IN" } } }); expect(string).toContain("`id` NOT IN (?)"); expect(values).toEqual([4]); }); test("empty IN matches nothing", () => { const { string, values } = gen({ query: { id: { value: [], equality: "IN" } } }); expect(string).toContain("1 = 0"); expect(values).toEqual([]); }); }); describe("targetId", () => { test("is always combined with AND", () => { const { where, whereValues } = gen( { query: { a: { value: 1 }, b: { value: 2 } }, searchOperator: "OR" }, { targetId: 5 }, ); expect(where).toBe("WHERE (`a` = ? OR `b` = ?) AND `id` = ?"); expect(whereValues).toEqual([1, 2, 5]); }); test("works without other conditions", () => { expect(gen({}, { targetId: "7" }).where).toBe("WHERE `id` = ?"); }); }); describe("where clause", () => { test("is separate from select-part values", () => { const res = gen({ countSubQueries: [ { table: "c", alias: "cc", srcTrgMap: [{ src: "uid", trg: "x" }] }, ], query: { id: { value: 1 } }, }); expect(res.values).toEqual(["x", 1]); expect(res.where).toBe("WHERE (`id` = ?)"); expect(res.whereValues).toEqual([1]); }); test("is empty without conditions", () => { expect(gen({ limit: 5 }).where).toBe(""); }); }); describe("select fields", () => { test("alias is applied once", () => { expect(gen({ selectFields: [{ fieldName: "name", alias: "n" }] }).string).toBe( "SELECT `name` AS `n` FROM `users`", ); }); test("object fields with joins use the field name", () => { const { string } = gen({ selectFields: [{ fieldName: "name" }], join: [{ tableName: "p", joinType: "LEFT JOIN", match: { source: "id", target: "uid" } }], }); expect(string).toBe( "SELECT `users`.`name`,`p`.* FROM `users` LEFT JOIN `p` ON `users`.`id` = `p`.`uid`", ); }); test("average uses AVG", () => { expect(gen({ selectFields: [{ fieldName: "n", average: true }] }).string).toContain("AVG(`n`)"); }); test("group_concat separator is escaped", () => { const { string } = gen({ selectFields: [{ fieldName: "n", group_concat: { alias: "g", separator: "'); DROP" } }], }); expect(string).toContain("GROUP_CONCAT(`n`, '''); DROP') AS `g`"); }); }); describe("pagination and ordering", () => { test("limit, page and order", () => { const { string } = gen({ group: ["a", { table: "p", field: "b" }], order: [ { field: "a", strategy: "asc" }, { field: "b", strategy: "DESC" }, ], limit: 10, page: 3, }); expect(string).toBe( "SELECT * FROM `users` GROUP BY `a`,`p`.`b` ORDER BY `a` ASC, `b` DESC LIMIT 10 OFFSET 20", ); }); test("offset without limit gets a LIMIT", () => { expect(gen({ offset: 5 }).string).toBe( "SELECT * FROM `users` LIMIT 18446744073709551615 OFFSET 5", ); }); test("count queries skip LIMIT and OFFSET", () => { expect(gen({ limit: 10, offset: 5 }, { count: true }).string).toBe("SELECT * FROM `users`"); }); test("full-text score ordering only when ordering by the score alias", () => { const fts = { fields: ["title"], searchTerm: "hi", scoreAlias: "score" }; expect(gen({ fullTextSearch: fts, order: { field: "score", strategy: "DESC" } }).string).toContain( "ORDER BY `score` DESC", ); expect(gen({ fullTextSearch: fts, order: { field: "id", strategy: "ASC" } }).string).toContain( "ORDER BY `id` ASC", ); }); });