@jakub.knejzlik/ts-query
Version:
TypeScript implementation of SQL builder
131 lines (109 loc) • 4.89 kB
text/typescript
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")');
});
});