Files
2026-07-13 13:09:14 +08:00

170 lines
8.5 KiB
TypeScript
Vendored

import { strict as assert } from "node:assert";
import { test } from "vitest";
import { allEditableColumnsWriteable, allPrimaryKeysPresent, analyzeEditableQuery, analyzeEditableQueryEditability, isBinaryType, queryEditabilityMessageKey, sourceColumnsForResult } from "../../apps/desktop/src/lib/sql/sqlAnalysis.ts";
test("recognizes a simple single-table SELECT as editable", () => {
const result = analyzeEditableQueryEditability("select id, name from public.users where active = true order by id");
assert.equal(result.editable, true);
assert.equal(result.analysis.schema, "public");
assert.equal(result.analysis.tableName, "users");
assert.equal(result.analysis.selectStar, false);
assert.deepEqual(result.analysis.columns, [
{ sourceName: "id", sourceNameQuoted: false, resultName: "id", expression: "id" },
{ sourceName: "name", sourceNameQuoted: false, resultName: "name", expression: "name" },
]);
});
test("recognizes quoted table names and table aliases", () => {
const result = analyzeEditableQueryEditability('SELECT u."id", u."full name" FROM "app schema"."user table" AS u');
assert.equal(result.editable, true);
assert.equal(result.analysis.schema, "app schema");
assert.equal(result.analysis.tableName, "user table");
assert.equal(result.analysis.tableAlias, "u");
assert.deepEqual(result.analysis.columns, [
{ sourceName: "id", sourceNameQuoted: true, sourceQualifier: "u", sourceKey: "u:0", resultName: "id", expression: 'u."id"' },
{ sourceName: "full name", sourceNameQuoted: true, sourceQualifier: "u", sourceKey: "u:0", resultName: "full name", expression: 'u."full name"' },
]);
});
test("keeps the legacy analyzer API for editable SELECT queries", () => {
const analysis = analyzeEditableQuery("select * from users");
assert.ok(analysis);
assert.equal(analysis.tableName, "users");
assert.equal(analysis.selectStar, true);
assert.deepEqual(analysis.columns, []);
});
test("maps joined query source columns for column-level editing", () => {
const result = analyzeEditableQueryEditability("select u.id as user_id, u.name, o.total from users u join orders o on o.user_id = u.id");
assert.equal(result.editable, true);
assert.equal(result.analysis.multiSource, true);
assert.equal(result.analysis.allowInsertDelete, false);
assert.deepEqual(
result.analysis.sources?.map((source) => ({ key: source.key, tableName: source.tableName, alias: source.alias })),
[
{ key: "u:0", tableName: "users", alias: "u" },
{ key: "o:1", tableName: "orders", alias: "o" },
],
);
assert.deepEqual(result.analysis.columns, [
{ sourceName: "id", sourceNameQuoted: false, sourceQualifier: "u", sourceKey: "u:0", resultName: "user_id", expression: "u.id" },
{ sourceName: "name", sourceNameQuoted: false, sourceQualifier: "u", sourceKey: "u:0", resultName: "name", expression: "u.name" },
{ sourceName: "total", sourceNameQuoted: false, sourceQualifier: "o", sourceKey: "o:1", resultName: "total", expression: "o.total" },
]);
assert.equal(allPrimaryKeysPresent(["id"], ["user_id", "name", "total"], result.analysis, "u:0"), true);
assert.deepEqual(sourceColumnsForResult(result.analysis, ["user_id", "name", "total"], "u:0"), ["id", "name", undefined]);
});
test("recognizes single-table explicit columns mixed with alias star", () => {
const result = analyzeEditableQueryEditability("select t.create_date, t.* from tt_kd_material_container_sap t where t.order_no = 'KD2607071336' order by t.create_date desc");
assert.equal(result.editable, true);
assert.equal(result.analysis.tableName, "tt_kd_material_container_sap");
assert.equal(result.analysis.tableAlias, "t");
assert.deepEqual(result.analysis.columns, [
{ sourceName: "create_date", sourceNameQuoted: false, sourceQualifier: "t", sourceKey: "t:0", resultName: "create_date", expression: "t.create_date" },
{ star: true, sourceQualifier: "t", sourceKey: "t:0", resultName: "*", expression: "t.*" },
]);
});
test("treats DISTINCT single-table projections as update-only", () => {
const result = analyzeEditableQueryEditability("select distinct id, name from users");
assert.equal(result.editable, true);
assert.equal(result.analysis.distinct, true);
assert.equal(result.analysis.allowInsertDelete, false);
assert.deepEqual(result.analysis.columns, [
{ sourceName: "id", sourceNameQuoted: false, resultName: "id", expression: "id" },
{ sourceName: "name", sourceNameQuoted: false, resultName: "name", expression: "name" },
]);
});
test("maps a DISTINCT qualified star to one joined source", () => {
const result = analyzeEditableQueryEditability("select distinct u.* from users u left join orders o on o.user_id = u.id");
assert.equal(result.editable, true);
assert.equal(result.analysis.distinct, true);
assert.equal(result.analysis.multiSource, true);
assert.equal(result.analysis.allowInsertDelete, false);
assert.deepEqual(result.analysis.columns, [{ star: true, sourceQualifier: "u", sourceKey: "u:0", resultName: "*", expression: "u.*" }]);
});
test("keeps ambiguous DISTINCT projections read-only", () => {
assert.deepEqual(analyzeEditableQueryEditability("select distinct * from users u join orders o on o.user_id = u.id"), {
editable: false,
reason: "complex-source",
});
assert.deepEqual(analyzeEditableQueryEditability("select distinct on (user_id) id, user_id from orders order by user_id, id desc"), {
editable: false,
reason: "aggregation",
});
});
test("reports DuckDB external file scans as read-only external sources", () => {
const result = analyzeEditableQueryEditability("SELECT * FROM '/tmp/duckdb_excel_extension_test.xlsx'");
assert.deepEqual(result, {
editable: false,
reason: "external-source",
});
assert.equal(queryEditabilityMessageKey(result.reason), "grid.queryEditUnsupported.external-source");
});
test("reports computed result columns as unsafe to edit", () => {
const result = analyzeEditableQueryEditability("select id, count(*) as total from users group by id");
assert.deepEqual(result, {
editable: false,
reason: "aggregation",
});
});
test("keeps single-table expression columns while mapping writable source columns", () => {
const result = analyzeEditableQueryEditability("select iso3, year, country_name, ihli / gdp_pc as score from ihli_data");
assert.equal(result.editable, true);
assert.deepEqual(result.analysis.columns, [
{ sourceName: "iso3", sourceNameQuoted: false, resultName: "iso3", expression: "iso3" },
{ sourceName: "year", sourceNameQuoted: false, resultName: "year", expression: "year" },
{ sourceName: "country_name", sourceNameQuoted: false, resultName: "country_name", expression: "country_name" },
{ sourceName: undefined, sourceNameQuoted: false, resultName: "score", expression: "ihli / gdp_pc" },
]);
assert.equal(allPrimaryKeysPresent(["iso3", "year"], ["iso3", "year", "country_name", "score"], result.analysis), true);
assert.equal(allEditableColumnsWriteable(result.analysis, ["iso3", "year", "country_name", "score"]), true);
});
test("accepts aliased primary key source columns for row identity", () => {
const analysis = analyzeEditableQuery("select id as user_id, name from users");
assert.ok(analysis);
assert.equal(allPrimaryKeysPresent(["id"], ["user_id", "name"], analysis), true);
assert.equal(allEditableColumnsWriteable(analysis, ["user_id", "name"]), true);
assert.equal(allPrimaryKeysPresent(["id"], ["id", "name"], analyzeEditableQuery("select id, name from users")!), true);
});
test("maps ClickHouse simple query results when identifier columns are returned", () => {
const analysis = analyzeEditableQuery("SELECT id, name, score + 1 AS next_score FROM default.people");
assert.ok(analysis);
assert.equal(allPrimaryKeysPresent(["id"], ["id", "name", "next_score"], analysis), true);
assert.equal(allEditableColumnsWriteable(analysis, ["id", "name", "next_score"]), true);
assert.deepEqual(sourceColumnsForResult(analysis, ["id", "name", "next_score"]), ["id", "name", undefined]);
});
test("rejects ClickHouse query result editing when identifier source columns are omitted", () => {
const analysis = analyzeEditableQuery("SELECT name FROM default.people");
assert.ok(analysis);
assert.equal(allPrimaryKeysPresent(["id"], ["name"], analysis), false);
assert.deepEqual(sourceColumnsForResult(analysis, ["name"]), ["name"]);
});
test("recognizes binary type declarations with lengths", () => {
assert.equal(isBinaryType("binary(16)"), true);
assert.equal(isBinaryType("VARBINARY(255)"), true);
assert.equal(isBinaryType("varchar(255)"), false);
});