175 lines
6.5 KiB
TypeScript
Vendored
175 lines
6.5 KiB
TypeScript
Vendored
import { strict as assert } from "node:assert";
|
|
import { test } from "vitest";
|
|
import { buildInsertValueHints, expandToSqlStatementWindow, parseInsertValueHints, parseInsertValueHintsInRanges, parseInsertValuesClauses } from "../../apps/desktop/src/lib/sql/insertValueHints.ts";
|
|
|
|
test("maps explicit column list to single-row VALUES", () => {
|
|
const sql = "INSERT INTO auth_user (id, password, last_login) VALUES (5, 'hash', NULL)";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => ({ column: hint.column, text: sql.slice(hint.from, hint.from + 1) })),
|
|
[
|
|
{ column: "id", text: "5" },
|
|
{ column: "password", text: "'" },
|
|
{ column: "last_login", text: "N" },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("supports multi-row VALUES", () => {
|
|
const sql = "INSERT INTO users (id, name) VALUES (1, 'a'), (2, 'b')";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["id", "name", "id", "name"],
|
|
);
|
|
assert.equal(sql.slice(hints[0]!.from, hints[0]!.from + 1), "1");
|
|
assert.equal(sql.slice(hints[2]!.from, hints[2]!.from + 1), "2");
|
|
});
|
|
|
|
test("does not split nested parentheses inside a value", () => {
|
|
const sql = "INSERT INTO t (a, b) VALUES (COALESCE(x, y), NOW())";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["a", "b"],
|
|
);
|
|
assert.ok(sql.slice(hints[0]!.from).startsWith("COALESCE(x, y)"));
|
|
assert.ok(sql.slice(hints[1]!.from).startsWith("NOW()"));
|
|
});
|
|
|
|
test("does not split PostgreSQL dollar-quoted values", () => {
|
|
const sql = "INSERT INTO t (body, count) VALUES ($tag$hello,(world),again$tag$, 2)";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["body", "count"],
|
|
);
|
|
assert.ok(sql.slice(hints[0]!.from).startsWith("$tag$hello,(world),again$tag$"));
|
|
assert.equal(sql.slice(hints[1]!.from, hints[1]!.from + 1), "2");
|
|
});
|
|
|
|
test("skips SQL Server table hints before the INSERT column list", () => {
|
|
const sql = "INSERT INTO dbo.Users WITH (TABLOCK) (id, name) VALUES (1, 'alice')";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["id", "name"],
|
|
);
|
|
});
|
|
|
|
test("resolves columns from table metadata when column list is omitted", () => {
|
|
const sql = "INSERT INTO users VALUES (1, 'alice')";
|
|
const hints = parseInsertValueHints(sql, {
|
|
resolveTableColumns: (table) => (table === "users" ? ["id", "name"] : undefined),
|
|
});
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["id", "name"],
|
|
);
|
|
});
|
|
|
|
test("returns no hints for INSERT ... SELECT", () => {
|
|
const sql = "INSERT INTO users (id, name) SELECT id, name FROM staging";
|
|
assert.deepEqual(parseInsertValueHints(sql), []);
|
|
assert.deepEqual(parseInsertValuesClauses(sql), []);
|
|
});
|
|
|
|
test("caps hints when value count exceeds column count", () => {
|
|
const sql = "INSERT INTO t (a, b) VALUES (1, 2, 3)";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["a", "b"],
|
|
);
|
|
});
|
|
|
|
test("caps hints when column count exceeds value count", () => {
|
|
const sql = "INSERT INTO t (a, b, c) VALUES (1, 2)";
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["a", "b"],
|
|
);
|
|
});
|
|
|
|
test("handles quoted identifiers in column list", () => {
|
|
const sql = 'INSERT INTO "User" ("Id", "Name") VALUES (1, \'x\')';
|
|
const hints = parseInsertValueHints(sql);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["Id", "Name"],
|
|
);
|
|
});
|
|
|
|
test("parses schema-qualified table without column list", () => {
|
|
const clauses = parseInsertValuesClauses("INSERT INTO dbo.Users VALUES (1)");
|
|
assert.equal(clauses.length, 1);
|
|
assert.equal(clauses[0]?.table, "Users");
|
|
assert.equal(clauses[0]?.schema, "dbo");
|
|
assert.equal(clauses[0]?.database, undefined);
|
|
assert.equal(clauses[0]?.columns, null);
|
|
});
|
|
|
|
test("preserves three-part database.schema.table qualifiers", () => {
|
|
const clauses = parseInsertValuesClauses("INSERT INTO OtherDb.dbo.Users VALUES (1, 'a')");
|
|
assert.equal(clauses.length, 1);
|
|
assert.equal(clauses[0]?.database, "OtherDb");
|
|
assert.equal(clauses[0]?.schema, "dbo");
|
|
assert.equal(clauses[0]?.table, "Users");
|
|
});
|
|
|
|
test("preserves quoted three-part database.schema.table qualifiers", () => {
|
|
const clauses = parseInsertValuesClauses('INSERT INTO "OtherDb"."dbo"."Users" VALUES (1)');
|
|
assert.equal(clauses[0]?.database, "OtherDb");
|
|
assert.equal(clauses[0]?.schema, "dbo");
|
|
assert.equal(clauses[0]?.table, "Users");
|
|
});
|
|
|
|
test("routes three-part names through resolveTableColumns database argument", () => {
|
|
const sql = "INSERT INTO OtherDb.dbo.Users VALUES (1, 'a')";
|
|
const calls: Array<{ table: string; schema?: string; database?: string }> = [];
|
|
const hints = parseInsertValueHints(sql, {
|
|
resolveTableColumns: (table, schema, database) => {
|
|
calls.push({ table, schema, database });
|
|
if (database === "OtherDb" && schema === "dbo" && table === "Users") return ["id", "name"];
|
|
return ["wrong_id"];
|
|
},
|
|
});
|
|
assert.deepEqual(calls, [{ table: "Users", schema: "dbo", database: "OtherDb" }]);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["id", "name"],
|
|
);
|
|
});
|
|
|
|
test("parses only statement windows covering provided ranges", () => {
|
|
const prefix = `${"SELECT 1;\n".repeat(200)}`;
|
|
const insert = "INSERT INTO t (id, name) VALUES (1, 'x');";
|
|
const suffix = `\n${"SELECT 2;\n".repeat(200)}`;
|
|
const sql = `${prefix}${insert}${suffix}`;
|
|
const insertFrom = prefix.length;
|
|
const hints = parseInsertValueHintsInRanges(sql, [{ from: insertFrom, to: insertFrom + 10 }]);
|
|
assert.deepEqual(
|
|
hints.map((hint) => hint.column),
|
|
["id", "name"],
|
|
);
|
|
assert.equal(sql.slice(hints[0]!.from, hints[0]!.from + 1), "1");
|
|
});
|
|
|
|
test("expandToSqlStatementWindow stops at neighboring statements", () => {
|
|
const sql = "SELECT 1; INSERT INTO t (a) VALUES (1); SELECT 2;";
|
|
const insertAt = sql.indexOf("INSERT");
|
|
const window = expandToSqlStatementWindow(sql, insertAt, insertAt + 6);
|
|
assert.equal(sql.slice(window.from, window.to), "INSERT INTO t (a) VALUES (1)");
|
|
});
|
|
|
|
test("ignores statements that are not INSERT VALUES", () => {
|
|
const sql = "SELECT 1; UPDATE users SET name = 'a' WHERE id = 1;";
|
|
assert.deepEqual(parseInsertValueHints(sql), []);
|
|
});
|
|
|
|
test("buildInsertValueHints skips unresolved tables without metadata", () => {
|
|
const clauses = parseInsertValuesClauses("INSERT INTO mystery VALUES (1, 2)");
|
|
assert.deepEqual(buildInsertValueHints(clauses), []);
|
|
});
|