UNPKG

@jakub.knejzlik/ts-query

Version:

TypeScript implementation of SQL builder

131 lines (109 loc) 4.89 kB
import { Condition, Cond } from "./Condition"; import { Q } from "./Query"; import { MySQLFlavor } from "./flavors/mysql"; const flavor = new MySQLFlavor(); const roundtrip = (cond: Condition): Condition => Condition.deserialize(cond.serialize())!; describe("Cond.in with subquery", () => { const buildSubquery = () => Q.select().field("c").from("users").where(Cond.equal("active", 1)); it("toSQL produces `field IN (SELECT ...)` for a direct subquery", () => { const cond = Cond.in("x", buildSubquery()); expect(cond.toSQL(flavor)).toEqual( "`x` IN (SELECT `c` FROM `users` WHERE `active` = 1)" ); }); it("round-trips a subquery identically through serialize/deserialize", () => { const cond = Cond.in("x", buildSubquery()); const expected = cond.toSQL(flavor); const restored = roundtrip(cond); expect(restored.toSQL(flavor)).toEqual(expected); expect(restored.toSQL(flavor)).toEqual( "`x` IN (SELECT `c` FROM `users` WHERE `active` = 1)" ); }); it("survives a full query serialize round-trip (does NOT drop the WHERE)", () => { const q = Q.select() .field("id") .from("t") .where(Cond.in("x", buildSubquery())); const expected = q.toSQL(flavor); const restored = Q.deserialize(q.serialize()) as any; expect(restored.toSQL(flavor)).toEqual(expected); expect(restored.toSQL(flavor)).toContain("WHERE `x` IN (SELECT"); }); it("also accepts a single-element array containing a subquery", () => { const cond = Cond.in("x", [buildSubquery()] as any); expect(cond.toSQL(flavor)).toEqual( "`x` IN (SELECT `c` FROM `users` WHERE `active` = 1)" ); expect(roundtrip(cond).toSQL(flavor)).toEqual(cond.toSQL(flavor)); }); it("notIn supports subqueries and round-trips", () => { const cond = Cond.notIn("x", buildSubquery() as any); expect(cond.toSQL(flavor)).toEqual( "`x` NOT IN (SELECT `c` FROM `users` WHERE `active` = 1)" ); expect(roundtrip(cond).toSQL(flavor)).toEqual(cond.toSQL(flavor)); }); }); describe("Cond.in fail-closed for empty/null values", () => { it("empty array produces `field IN (NULL)` instead of dropping the filter", () => { const cond = Cond.in("foo", []); expect(cond).not.toBeNull(); expect(cond.toSQL(flavor)).toEqual("`foo` IN (NULL)"); }); it("null values produce `field IN (NULL)` (fail-closed)", () => { const cond = Cond.in("foo", null); expect(cond).not.toBeNull(); expect(cond.toSQL(flavor)).toEqual("`foo` IN (NULL)"); }); it("fail-closed condition survives serialize round-trip", () => { const cond = Cond.in("foo", []); expect(roundtrip(cond).toSQL(flavor)).toEqual("`foo` IN (NULL)"); }); it("empty IN never disappears from a serialized query (no data leak)", () => { const q = Q.select().field("id").from("t").where(Cond.in("foo", [])); const restored = Q.deserialize(q.serialize()) as any; expect(restored.toSQL(flavor)).toContain("WHERE `foo` IN (NULL)"); }); it("notIn empty array is fail-closed and valid SQL", () => { const cond = Cond.notIn("foo", []); expect(cond.toSQL(flavor)).toEqual("`foo` NOT IN (NULL)"); expect(roundtrip(cond).toSQL(flavor)).toEqual("`foo` NOT IN (NULL)"); }); }); describe("Cond.in rejects mixed subquery + values (fail-loud)", () => { const sub = () => Q.select().field("c").from("users").where(Cond.equal("active", 1)); const MSG = "Cond.in/notIn: cannot mix a subquery with other values or use multiple subqueries; pass a single subquery or a list of scalar values"; it("throws for [subquery, scalar, scalar] (subquery first)", () => { expect(() => Cond.in("x", [sub(), "a", "b"] as any)).toThrow(MSG); }); it("throws for [scalar, subquery] (subquery last)", () => { expect(() => Cond.in("x", ["a", sub()] as any)).toThrow(MSG); }); it("throws for [subquery, subquery] (multiple subqueries)", () => { expect(() => Cond.in("x", [sub(), sub()] as any)).toThrow(MSG); }); it("notIn throws for mixed arrays too", () => { expect(() => Cond.notIn("x", [sub(), "a"] as any)).toThrow(MSG); }); it("still accepts a clean single subquery and a single-element array", () => { expect(() => Cond.in("x", sub())).not.toThrow(); expect(() => Cond.in("x", [sub()] as any)).not.toThrow(); }); }); describe("Cond.in non-empty arrays are unchanged", () => { it("preserves numeric value lists", () => { const cond = Cond.in("foo", [1, 2, 3]); expect(cond.toSQL(flavor)).toEqual("`foo` IN (1, 2, 3)"); expect(roundtrip(cond).toSQL(flavor)).toEqual("`foo` IN (1, 2, 3)"); }); it("preserves string value lists", () => { const cond = Cond.in("type", ["A", "B"]); expect(cond.toSQL(flavor)).toEqual('`type` IN ("A", "B")'); expect(roundtrip(cond).toSQL(flavor)).toEqual('`type` IN ("A", "B")'); }); });