Files
dbx/apps/desktop/src/lib/__tests__/sql/sqlLayout.spec.ts
T

236 lines
12 KiB
TypeScript
Vendored

import { describe, expect, it } from "vitest";
import { formatSqlText } from "@/lib/sql/sqlFormatter";
import { DEFAULT_SQL_FORMATTER_SETTINGS, type SqlFormatterSettings } from "@/lib/sql/sqlFormatterConfig";
/**
* Golden tests for the layout engine behind DBX's default formatting style.
* The expectations for the queries in issues #827, #4850 and #5170 pin the
* real-world shapes those reports asked for; the rest pin the rules those
* examples exercise — the collapse of short statements, the wrap of long ones,
* element alignment, and column alignment in `CREATE TABLE`.
*/
type Dialect = Parameters<typeof formatSqlText>[1];
async function format(sql: string, overrides: Partial<SqlFormatterSettings> = {}, dialect: Dialect = "mysql") {
return formatSqlText(sql, dialect, { ...DEFAULT_SQL_FORMATTER_SETTINGS, ...overrides });
}
const lines = (...rows: string[]) => rows.join("\n");
describe("sql layout", () => {
it("formats a query with a derived table (#827)", async () => {
const sql = lines(
"SELECT loc_id, loc_name, loc_type, delivery_emp_num, update_time",
"FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY loc_id ORDER BY update_time DESC) AS rn",
" FROM responsible_area_info",
" WHERE DATE_FORMAT(update_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')) t",
"WHERE rn = 1;",
);
// The subquery's clauses align under the `(` that opens it, and the closing
// `)` stays on the last clause's line.
//
// The subquery's two select elements could be split; the screenshots in
// #827 and #4850 disagree on that point (both are two-element lists, one
// split, one not), so the keeping rule here is the one #4850's rendering
// matches.
expect(await format(sql)).toBe(
lines(
"SELECT loc_id,",
" loc_name,",
" loc_type,",
" delivery_emp_num,",
" update_time",
"FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY loc_id ORDER BY update_time DESC) AS rn",
" FROM responsible_area_info",
" WHERE DATE_FORMAT(update_time, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')) t",
"WHERE rn = 1;",
),
);
});
it("keeps a long statement's clauses apart (#4850)", async () => {
const sql = lines(
"SELECT HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID, HIMMS_XDSBDJ.YNKE, himms_zwxsysj_sysj(V_HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID) AS SJSYSJ",
"FROM HIMMS_XDSBDJ",
"LEFT JOIN (SELECT himms_xdsbdj_id, MAX(SJ) SJ FROM himms_zwxqdjc GROUP BY himms_xdsbdj_id) SUB_QDJC ON HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID = SUB_QDJC.himms_xdsbdj_id",
"WHERE HIMMS_XDSBDJ.tybz IS NULL;",
);
// The user-defined function is written without a space, and the join's
// subquery expands rather than pushing `ON` onto a line of its own.
expect(await format(sql)).toBe(
lines(
"SELECT HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID, HIMMS_XDSBDJ.YNKE, himms_zwxsysj_sysj(V_HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID) AS SJSYSJ",
"FROM HIMMS_XDSBDJ",
" LEFT JOIN (SELECT himms_xdsbdj_id, MAX(SJ) SJ",
" FROM himms_zwxqdjc",
" GROUP BY himms_xdsbdj_id) SUB_QDJC ON HIMMS_XDSBDJ.HIMMS_XDSBDJ_ID = SUB_QDJC.himms_xdsbdj_id",
"WHERE HIMMS_XDSBDJ.tybz IS NULL;",
),
);
});
it("collapses a statement that fits on one line (#5170)", async () => {
const sql = lines("SELECT", " *", "FROM", " task", "ORDER BY", " created_at DESC;");
expect(await format(sql)).toBe("SELECT * FROM task ORDER BY created_at DESC;");
});
it("aligns CREATE TABLE columns", async () => {
const sql = lines(
"CREATE TABLE IF NOT EXISTS `delivery_emp_info`",
"(",
" `id` varchar(20) PRIMARY KEY NOT NULL COMMENT '主键',",
" `emp_id` varchar(20) NOT NULL COMMENT '员工id',",
" `delivery_type_ids` varchar(512) DEFAULT NULL COMMENT '负责的运送类型id集合',",
" `update_time` datetime NOT NULL COMMENT '更新时间',",
" CONSTRAINT idx_emp_phone UNIQUE (emp_id, emp_phone_num) COMMENT '运送员id电话唯一'",
") ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin COMMENT '运达员信息表';",
);
// Name, type and modifiers each get their own column, so the definitions'
// `COMMENT`s line up; the constraint's `(` keeps its space.
expect(await format(sql)).toBe(
lines(
"CREATE TABLE IF NOT EXISTS `delivery_emp_info`",
"(",
" `id` varchar(20) PRIMARY KEY NOT NULL COMMENT '主键',",
" `emp_id` varchar(20) NOT NULL COMMENT '员工id',",
" `delivery_type_ids` varchar(512) DEFAULT NULL COMMENT '负责的运送类型id集合',",
" `update_time` datetime NOT NULL COMMENT '更新时间',",
" CONSTRAINT idx_emp_phone UNIQUE (emp_id, emp_phone_num) COMMENT '运送员id电话唯一'",
") ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_bin COMMENT '运达员信息表';",
),
);
});
it("keeps a call shaped like a call at any nesting depth", async () => {
const sql = "INSERT INTO t (a, b) VALUES (f(x), g(y)) ON DUPLICATE KEY UPDATE b = f(g(b));";
// The insert target's column list is spaced; the calls inside the values,
// and inside another call's arguments, are not.
expect(await format(sql)).toBe("INSERT INTO t (a, b) VALUES (f(x), g(y)) ON DUPLICATE KEY UPDATE b = f(g(b));");
});
it("breaks a long condition before each logical operator", async () => {
const sql = "SELECT a, b FROM t WHERE alpha = 1 AND beta = 2 AND gamma = 3 AND delta = 4 AND epsilon = 5 AND zeta = 6 AND eta = 7 AND theta = 8;";
// The statement is too long to collapse, and the default
// `logicalOperatorNewline: "before"` starts each continuation line with the
// operator, aligned under the condition's first operand like any other
// clause item.
expect(await format(sql)).toBe(lines("SELECT a, b", "FROM t", "WHERE alpha = 1", " AND beta = 2", " AND gamma = 3", " AND delta = 4", " AND epsilon = 5", " AND zeta = 6", " AND eta = 7", " AND theta = 8;"));
});
it("puts a wrapped condition's operators where the setting asks", async () => {
const sql = "SELECT a, b FROM t WHERE alpha = 1 AND beta = 2 AND gamma = 3 AND delta = 4 AND epsilon = 5 AND zeta = 6 AND eta = 7 AND theta = 8;";
// `after` ends the previous line with the operator instead.
expect(await format(sql, { logicalOperatorNewline: "after" })).toContain("WHERE alpha = 1 AND\n beta = 2 AND");
// `none` keeps the condition on the clause's line, however long it gets.
expect(await format(sql, { logicalOperatorNewline: "none" })).toBe(lines("SELECT a, b", "FROM t", "WHERE alpha = 1 AND beta = 2 AND gamma = 3 AND delta = 4 AND epsilon = 5 AND zeta = 6 AND eta = 7 AND theta = 8;"));
});
it("expands a parenthesized list that does not fit, aligned under its first item", async () => {
const sql = `SELECT * FROM t WHERE id IN (${Array.from({ length: 16 }, (_, index) => 100000 + index).join(", ")});`;
expect(await format(sql)).toBe(
lines(
"SELECT *",
"FROM t",
"WHERE id IN (100000,",
" 100001,",
" 100002,",
" 100003,",
" 100004,",
" 100005,",
" 100006,",
" 100007,",
" 100008,",
" 100009,",
" 100010,",
" 100011,",
" 100012,",
" 100013,",
" 100014,",
" 100015);",
),
);
});
it("keeps a small subquery inline", async () => {
expect(await format("SELECT * FROM t WHERE id IN (SELECT id FROM u WHERE a = 1 AND b = 2);")).toBe("SELECT * FROM t WHERE id IN (SELECT id FROM u WHERE a = 1 AND b = 2);");
});
it("collapses each clause independently", async () => {
const sql = "SELECT (SELECT count(*) FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid') AS order_count, c.name FROM customers c;";
// The scalar subquery fits, the select list does not: only the second one
// breaks.
expect(await format(sql)).toBe(lines("SELECT (SELECT count(*) FROM orders o WHERE o.customer_id = c.id AND o.status = 'paid') AS order_count, c.name", "FROM customers c;"));
});
it("never joins a CASE onto one line", async () => {
expect(await format("SELECT CASE WHEN a = 1 THEN 'one' WHEN a = 2 THEN 'two' ELSE 'other' END AS label FROM t;")).toBe(lines("SELECT CASE", " WHEN a = 1 THEN 'one'", " WHEN a = 2 THEN 'two'", " ELSE 'other'", " END AS label", "FROM t;"));
});
it("gives a set operation its own line", async () => {
expect(await format("SELECT a FROM t UNION ALL SELECT b FROM u;")).toBe(lines("SELECT a", "FROM t", "UNION ALL", "SELECT b", "FROM u;"));
});
it("keeps comments and does not fold them onto a line", async () => {
const sql = lines("-- heading", "SELECT a, -- why", " b", "FROM t; /* trailing */");
expect(await format(sql)).toBe(lines("-- heading", "SELECT a,", " -- why", " b", "FROM t;", "", "/* trailing */"));
});
it("keeps PostgreSQL casts tight", async () => {
expect(await format("select coalesce(a, b)::text, (x + y)::int from t;", {}, "postgres")).toBe("SELECT coalesce(a, b)::text, (x + y)::int FROM t;");
});
it("keeps the comma of a MySQL LIMIT offset", async () => {
expect(await format("SELECT a FROM t LIMIT 5, 10;", {}, "mysql")).toBe("SELECT a FROM t LIMIT 5, 10;");
});
it("indents with tabs when asked", async () => {
const sql = "SELECT alpha_column, beta_column, gamma_column, delta_column FROM some_really_long_table_name WHERE first_condition = 1 AND second_condition = 2 AND third_condition = 3;";
// Only the leading indentation becomes a tab; the alignment padding stays
// spaces so a continuation column lands where it was measured.
expect(await format(sql, { useTabs: true, tabWidth: 4 })).toBe(["SELECT alpha_column,", "\t beta_column,", "\t gamma_column,", "\t delta_column", "FROM some_really_long_table_name", "WHERE first_condition = 1", "\t AND second_condition = 2", "\t AND third_condition = 3;"].join("\n"));
});
it("moves a FROM source to its own line when configured", async () => {
const sql = "SELECT alpha_column, beta_column, gamma_column, delta_column FROM some_really_long_table_name WHERE first_condition = 1 AND second_condition = 2 AND third_condition = 3;";
expect(await format(sql, { fromClauseLayout: "newLine" })).toBe(
lines("SELECT alpha_column,", " beta_column,", " gamma_column,", " delta_column", "FROM", " some_really_long_table_name", "WHERE first_condition = 1", " AND second_condition = 2", " AND third_condition = 3;"),
);
});
it("honours the blank lines between queries", async () => {
const sql = "SELECT 1;\nSELECT 2;";
expect(await format(sql, { linesBetweenQueries: 0 })).toBe(lines("SELECT 1;", "SELECT 2;"));
expect(await format(sql, { linesBetweenQueries: 1 })).toBe(lines("SELECT 1;", "", "SELECT 2;"));
expect(await format(sql, { linesBetweenQueries: 2 })).toBe(lines("SELECT 1;", "", "", "SELECT 2;"));
});
it("keeps a blank source line when the setting is on", async () => {
expect(await format(lines("-- heading", "", "SELECT a FROM t;"), { preserveEmptyLines: true })).toBe(lines("-- heading", "", "SELECT a", "FROM t;"));
});
it("falls back to sql-formatter for the tabular indent styles", async () => {
// The tabular layouts pad every clause to a fixed column; the default
// layout rules do not reproduce them, so those settings keep the upstream
// formatter.
expect(await format("SELECT a, b FROM t WHERE x = 1;", { indentStyle: "tabularLeft" })).toBe(lines("SELECT a,", " b", "FROM t", "WHERE x = 1;"));
});
it("formats a dialect without its own grammar", async () => {
expect(await format("select * from t where a = 1 and b = 2;", {}, "generic")).toBe("SELECT * FROM t WHERE a = 1 AND b = 2;");
});
});