226 lines
9.9 KiB
TypeScript
Vendored
226 lines
9.9 KiB
TypeScript
Vendored
import { describe, expect, it } from "vitest";
|
|
import { buildQueryWithHiddenPrimaryKeys, hiddenResultColumnIndexes } from "@/lib/sql/editableQueryHiddenKeys";
|
|
import { analyzeEditableQueryEditability, sourceColumnsForResult } from "@/lib/sql/sqlAnalysis";
|
|
|
|
describe("editable query hidden primary keys", () => {
|
|
it("appends quoted MySQL primary keys without changing the visible projection", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT name FROM users WHERE active = 1",
|
|
databaseType: "mysql",
|
|
primaryKeys: ["id"],
|
|
existingResultNames: ["name"],
|
|
}),
|
|
).toEqual({
|
|
sql: "SELECT name, `id` AS `__DBX_PK_0` FROM users WHERE active = 1",
|
|
projections: [{ sourceName: "id", alias: "__DBX_PK_0" }],
|
|
});
|
|
});
|
|
|
|
it("supports PostgreSQL composite primary keys and avoids alias collisions", () => {
|
|
const result = buildQueryWithHiddenPrimaryKeys({
|
|
sql: 'SELECT "value"\nFROM "items"',
|
|
databaseType: "postgres",
|
|
primaryKeys: ["tenant_id", "item_id"],
|
|
existingResultNames: ["value", "__DBX_PK_0"],
|
|
});
|
|
|
|
expect(result?.sql).toBe('SELECT "value", "tenant_id" AS "__DBX_PK_1", "item_id" AS "__DBX_PK_2"\nFROM "items"');
|
|
expect(result?.projections.map((projection) => projection.alias)).toEqual(["__DBX_PK_1", "__DBX_PK_2"]);
|
|
});
|
|
|
|
it("preserves SQL Server TOP and Oracle optimizer hints", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT TOP 10 name FROM dbo.users ORDER BY name",
|
|
databaseType: "sqlserver",
|
|
primaryKeys: ["id"],
|
|
existingResultNames: ["name"],
|
|
})?.sql,
|
|
).toBe("SELECT TOP 10 name, [id] AS [__DBX_PK_0] FROM dbo.users ORDER BY name");
|
|
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT /*+ INDEX(t IDX_USERS_NAME) */ t.NAME\nFROM USERS t",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["ID"],
|
|
existingResultNames: ["NAME"],
|
|
})?.sql,
|
|
).toBe('SELECT /*+ INDEX(t IDX_USERS_NAME) */ t.NAME, "ID" AS "__DBX_PK_0"\nFROM USERS t');
|
|
});
|
|
|
|
it("supports an Oracle ROWID expression for keyless base tables", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT * FROM APP.USERS t WHERE t.ACTIVE = 1",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "NAME"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
}),
|
|
).toEqual({
|
|
sql: 'SELECT t.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS t WHERE t.ACTIVE = 1',
|
|
projections: [{ sourceName: "__DBX_ROWID", alias: "__DBX_PK_0" }],
|
|
});
|
|
});
|
|
|
|
it("preserves an Oracle FOR UPDATE clause when appending a hidden row key", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT * FROM APP.USERS FOR UPDATE SKIP LOCKED",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "NAME"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe('SELECT USERS.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS FOR UPDATE SKIP LOCKED');
|
|
});
|
|
|
|
it("qualifies a bare Oracle wildcard when appending a hidden key", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: 'SELECT /*+ FULL("Users") */ * FROM APP."Users"',
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "NAME"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe('SELECT /*+ FULL("Users") */ "Users".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP."Users"');
|
|
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: 'SELECT * FROM APP.USERS "u"',
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "NAME"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe('SELECT "u".*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.USERS "u"');
|
|
});
|
|
|
|
it("rewrites the reported Oracle queries without changing their filters", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "select * from t_zyys_vte_yyfxbd",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "MBMC"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe('select t_zyys_vte_yyfxbd.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" from t_zyys_vte_yyfxbd');
|
|
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT * from T_XT_MB WHERE mbmc ='结束时'",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "MBMC"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe(`SELECT T_XT_MB.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" from T_XT_MB WHERE mbmc ='结束时'`);
|
|
});
|
|
|
|
it("preserves a WHERE subquery when adding an Oracle ROWID", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT t.* FROM APP.PLATFORM_CARS t WHERE t.CUSTOMER_NO IN (SELECT c.CUSTOMER_NO FROM APP.CUSTOMERS c WHERE c.ENABLED = 1)",
|
|
databaseType: "oracle",
|
|
primaryKeys: ["__DBX_ROWID"],
|
|
existingResultNames: ["ID", "CUSTOMER_NO"],
|
|
sourceExpressions: { __DBX_ROWID: "ROWIDTOCHAR(ROWID)" },
|
|
})?.sql,
|
|
).toBe('SELECT t.*, ROWIDTOCHAR(ROWID) AS "__DBX_PK_0" FROM APP.PLATFORM_CARS t WHERE t.CUSTOMER_NO IN (SELECT c.CUSTOMER_NO FROM APP.CUSTOMERS c WHERE c.ENABLED = 1)');
|
|
});
|
|
|
|
it("inserts hidden keys before a trailing line comment", () => {
|
|
expect(
|
|
buildQueryWithHiddenPrimaryKeys({
|
|
sql: "SELECT name -- visible user name\nFROM users",
|
|
databaseType: "mysql",
|
|
primaryKeys: ["id"],
|
|
existingResultNames: ["name"],
|
|
})?.sql,
|
|
).toBe("SELECT name, `id` AS `__DBX_PK_0` -- visible user name\nFROM users");
|
|
});
|
|
|
|
it("analyzes SQL Server TOP projections as ordinary source columns", () => {
|
|
for (const sql of [
|
|
"SELECT TOP 2 name, note FROM dbo.users ORDER BY name",
|
|
"SELECT TOP(2) name, note FROM dbo.users ORDER BY name",
|
|
"SELECT TOP (2) name, note FROM dbo.users ORDER BY name",
|
|
"SELECT TOP 10 PERCENT name, note FROM dbo.users ORDER BY name",
|
|
"SELECT TOP (2) WITH TIES name, note FROM dbo.users ORDER BY name",
|
|
]) {
|
|
const result = analyzeEditableQueryEditability(sql);
|
|
expect(result.editable, sql).toBe(true);
|
|
if (result.editable) expect(result.analysis.columns.map((column) => column.sourceName)).toEqual(["name", "note"]);
|
|
}
|
|
});
|
|
|
|
it("maps MySQL JSON expressions with implicit aliases without making them writeable", () => {
|
|
const result = analyzeEditableQueryEditability("select id, status, extra->>'$.mode' mode, extra->>'$.template' tmpl from jobs");
|
|
|
|
expect(result.editable).toBe(true);
|
|
if (!result.editable) return;
|
|
expect(result.analysis.columns).toEqual([
|
|
expect.objectContaining({ sourceName: "id", resultName: "id", expression: "id" }),
|
|
expect.objectContaining({ sourceName: "status", resultName: "status", expression: "status" }),
|
|
expect.objectContaining({ sourceName: undefined, resultName: "mode", expression: "extra->>'$.mode'" }),
|
|
expect.objectContaining({ sourceName: undefined, resultName: "tmpl", expression: "extra->>'$.template'" }),
|
|
]);
|
|
expect(sourceColumnsForResult(result.analysis, ["id", "status", "mode", "tmpl"])).toEqual(["id", "status", undefined, undefined]);
|
|
});
|
|
|
|
it("uses MySQL expression text as the result label when no alias is present", () => {
|
|
const result = analyzeEditableQueryEditability("select id, status, extra->>'$.mode', extra->>'$.template' from jobs");
|
|
|
|
expect(result.editable).toBe(true);
|
|
if (!result.editable) return;
|
|
expect(sourceColumnsForResult(result.analysis, ["id", "status", "extra->>'$.mode'", "extra->>'$.template'"])).toEqual(["id", "status", undefined, undefined]);
|
|
});
|
|
|
|
it("preserves direct columns and explicit aliases while keeping computed projections read-only", () => {
|
|
const result = analyzeEditableQueryEditability("select id, status as state, lower(name) normalized, count_value + 1 from jobs");
|
|
|
|
expect(result.editable).toBe(true);
|
|
if (!result.editable) return;
|
|
expect(sourceColumnsForResult(result.analysis, ["id", "state", "normalized", "count_value + 1"])).toEqual(["id", "status", undefined, undefined]);
|
|
});
|
|
|
|
it("does not mistake the right operand of an unaliased expression for an alias", () => {
|
|
const result = analyzeEditableQueryEditability("select id, id + status, lower(status) from jobs");
|
|
|
|
expect(result.editable).toBe(true);
|
|
if (!result.editable) return;
|
|
expect(sourceColumnsForResult(result.analysis, ["id", "id + status", "lower(status)"])).toEqual(["id", undefined, undefined]);
|
|
});
|
|
|
|
it("keeps aggregate, join-star, and set-operation boundaries read-only", () => {
|
|
expect(analyzeEditableQueryEditability("select id, count(*) total from jobs group by id")).toEqual({ editable: false, reason: "aggregation" });
|
|
expect(analyzeEditableQueryEditability("select * from jobs j join users u on u.id = j.user_id")).toEqual({ editable: false, reason: "complex-source" });
|
|
expect(analyzeEditableQueryEditability("select id from jobs union select id from archived_jobs")).toEqual({ editable: false, reason: "set-operation" });
|
|
});
|
|
|
|
it("resolves appended aliases to result indexes", () => {
|
|
expect(
|
|
hiddenResultColumnIndexes(
|
|
["name", "__DBX_PK_0", "__DBX_PK_1"],
|
|
[
|
|
{ sourceName: "tenant_id", alias: "__DBX_PK_0" },
|
|
{ sourceName: "item_id", alias: "__DBX_PK_1" },
|
|
],
|
|
),
|
|
).toEqual([1, 2]);
|
|
expect(hiddenResultColumnIndexes(["name"], [{ sourceName: "id", alias: "__DBX_PK_0" }])).toEqual([]);
|
|
expect(
|
|
hiddenResultColumnIndexes(
|
|
["name", "__DBX_PK_1"],
|
|
[
|
|
{ sourceName: "tenant_id", alias: "__DBX_PK_0" },
|
|
{ sourceName: "item_id", alias: "__DBX_PK_1" },
|
|
],
|
|
),
|
|
).toEqual([1]);
|
|
});
|
|
});
|