1273 lines
59 KiB
TypeScript
Vendored
1273 lines
59 KiB
TypeScript
Vendored
import { describe, expect, it } from "vitest";
|
|
import { executionCandidateForMode } from "@/lib/sql/sqlExecutionTarget";
|
|
import { buildExecutionCandidates, currentExecutableStatementRange, executableStatementRanges, fullSqlRange, hasMultipleExecutionTargets, splitSqlStatementRanges, statementRangeAtCursor, supportsExecutionTargetPicker } from "@/lib/sql/sqlStatementRanges";
|
|
|
|
function indexOf(sql: string, needle: string, occurrence = 1): number {
|
|
let from = 0;
|
|
let idx = -1;
|
|
for (let i = 0; i < occurrence; i += 1) {
|
|
idx = sql.indexOf(needle, from);
|
|
if (idx === -1) return -1;
|
|
from = idx + needle.length;
|
|
}
|
|
return idx;
|
|
}
|
|
|
|
function rangeSqlTexts(ranges: Array<{ sql: string }>): string[] {
|
|
return ranges.map((range) => range.sql.trim());
|
|
}
|
|
|
|
function candidateKinds(candidates: Array<{ kind: string }>): string[] {
|
|
return candidates.map((candidate) => candidate.kind);
|
|
}
|
|
|
|
function candidateLabels(candidates: Array<{ label: string }>): string[] {
|
|
return candidates.map((candidate) => candidate.label);
|
|
}
|
|
|
|
function candidateSummaries(candidates: Array<{ kind: string; sql: string }>): string[] {
|
|
return candidates.map((candidate) => `${candidate.kind}:${candidate.sql.trim()}`);
|
|
}
|
|
|
|
const oraclePlSqlFixture = `DECLARE
|
|
v_order_count NUMBER;
|
|
BEGIN
|
|
SELECT COUNT(*) INTO v_order_count
|
|
FROM "DBX_TEST"."ORDERS_10K";
|
|
|
|
IF v_order_count = 0 THEN
|
|
INSERT INTO "DBX_TEST"."STORES"
|
|
("ID", "STORE_CODE", "STORE_NAME", "CITY", "OPENED_AT")
|
|
SELECT 10001, 'TEST_STORE_001', '测试门店', '上海', SYSDATE
|
|
FROM DUAL
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM "DBX_TEST"."STORES" WHERE "ID" = 10001
|
|
);
|
|
|
|
INSERT INTO "DBX_TEST"."PRODUCTS"
|
|
("ID", "SKU", "PRODUCT_NAME", "CATEGORY", "PRICE")
|
|
SELECT 10001, 'TEST_SKU_001', '测试商品', '测试分类', 99.90
|
|
FROM DUAL
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM "DBX_TEST"."PRODUCTS" WHERE "ID" = 10001
|
|
);
|
|
|
|
INSERT INTO "DBX_TEST"."ORDERS_10K"
|
|
("ID", "ORDER_NO", "STORE_ID", "PRODUCT_ID", "CUSTOMER_NAME", "QUANTITY", "AMOUNT", "ORDER_STATUS", "CREATED_AT")
|
|
SELECT 10001, 'TEST_ORDER_001', 10001, 10001, '测试客户', 2, 199.80, 'PAID', SYSDATE
|
|
FROM DUAL
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM "DBX_TEST"."ORDERS_10K" WHERE "ORDER_NO" = 'TEST_ORDER_001'
|
|
);
|
|
|
|
COMMIT;
|
|
END IF;
|
|
END;
|
|
/
|
|
SELECT 1;`;
|
|
|
|
const oracleIssue2405PlSql = `DECLARE
|
|
PRE_TRD_DATE INTEGER ;
|
|
BEGIN
|
|
SELECT 1 + 2 INTO PRE_TRD_DATE FROM DUAL;
|
|
END;`;
|
|
|
|
const gaussDbNestedProcedure = `CREATE OR REPLACE PROCEDURE public.dbx_issue_4318()
|
|
AS
|
|
BEGIN
|
|
BEGIN
|
|
NULL;
|
|
END;
|
|
NULL;
|
|
END;`;
|
|
|
|
const gaussDbDollarQuotedFunctionScript = `DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid;
|
|
|
|
CREATE OR REPLACE FUNCTION dbx_issue_4572_tmp_md5_uuid (v_str IN TEXT) RETURNS varchar(36) LANGUAGE PLPGSQL IMMUTABLE AS $function$
|
|
DECLARE
|
|
str1 TEXT;
|
|
BEGIN
|
|
str1 := md5(v_str);
|
|
RETURN CAST(str1 AS varchar(36));
|
|
END$function$;
|
|
|
|
DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing;`;
|
|
|
|
const gaussDbIssue4573Script = `CREATE OR REPLACE PROCEDURE createIndex (
|
|
dbName IN VARCHAR(32),
|
|
tableName IN VARCHAR(64),
|
|
indexInfo IN VARCHAR(64),
|
|
indexColumns IN VARCHAR(128)
|
|
) AS
|
|
DECLARE STMT TEXT;
|
|
|
|
DECLARE flag int;
|
|
|
|
BEGIN
|
|
SELECT
|
|
count(*) INTO flag
|
|
FROM
|
|
PG_CATALOG.PG_INDEXES
|
|
WHERE
|
|
schemaname = dbName
|
|
AND TABLENAME = tableName
|
|
AND INDEXNAME = indexInfo;
|
|
|
|
IF flag = 0 THEN STMT := 'CREATE INDEX ' || indexInfo || ' ON ' || dbName || '.' || tableName || '(' || indexColumns || ')';
|
|
|
|
EXECUTE STMT;
|
|
|
|
END IF;
|
|
|
|
END;
|
|
|
|
SELECT 1 AS after_procedure;
|
|
SELECT 2 AS final_statement;`;
|
|
|
|
const xuguProgrammableObjectFixtures = [
|
|
`CREATE OR REPLACE PROCEDURE dbx_xugu_procedure AS
|
|
v_value INTEGER;
|
|
BEGIN
|
|
v_value := 1;
|
|
END;`,
|
|
`CREATE PROCEDURE dbx_xugu_procedure_without_replace AS
|
|
v_value INTEGER;
|
|
BEGIN
|
|
v_value := 1;
|
|
END;`,
|
|
`CREATE OR REPLACE FUNCTION dbx_xugu_function RETURN INTEGER AS
|
|
BEGIN
|
|
RETURN 1;
|
|
END;`,
|
|
`CREATE FUNCTION dbx_xugu_function_without_replace RETURN INTEGER AS
|
|
BEGIN
|
|
RETURN 1;
|
|
END;`,
|
|
`CREATE OR REPLACE TRIGGER dbx_xugu_trigger
|
|
BEFORE INSERT ON dbx_xugu_events
|
|
FOR EACH ROW
|
|
BEGIN
|
|
NULL;
|
|
END;`,
|
|
`CREATE TRIGGER dbx_xugu_trigger_without_replace
|
|
BEFORE INSERT ON dbx_xugu_events
|
|
FOR EACH ROW
|
|
BEGIN
|
|
NULL;
|
|
END;`,
|
|
`CREATE OR REPLACE PACKAGE BODY dbx_xugu_package AS
|
|
PROCEDURE ping AS
|
|
BEGIN
|
|
NULL;
|
|
END ping;
|
|
END dbx_xugu_package;`,
|
|
`CREATE PACKAGE BODY dbx_xugu_package_without_replace AS
|
|
PROCEDURE ping AS
|
|
BEGIN
|
|
NULL;
|
|
END ping;
|
|
END dbx_xugu_package_without_replace;`,
|
|
`CREATE OR REPLACE FORCE PACKAGE BODY dbx_xugu_force_package AS
|
|
PROCEDURE ping AS
|
|
BEGIN
|
|
NULL;
|
|
END ping;
|
|
END dbx_xugu_force_package;`,
|
|
`CREATE OR REPLACE NOFORCE PACKAGE BODY dbx_xugu_noforce_package AS
|
|
PROCEDURE ping AS
|
|
BEGIN
|
|
NULL;
|
|
END ping;
|
|
END dbx_xugu_noforce_package;`,
|
|
`CREATE OR REPLACE TYPE BODY dbx_xugu_type AS
|
|
MEMBER PROCEDURE ping IS
|
|
BEGIN
|
|
NULL;
|
|
END;
|
|
END;`,
|
|
`CREATE TYPE BODY dbx_xugu_type_without_replace AS
|
|
MEMBER PROCEDURE ping IS
|
|
BEGIN
|
|
NULL;
|
|
END;
|
|
END;`,
|
|
];
|
|
|
|
const mysqlRoutineFixture = `CREATE PROCEDURE p()
|
|
BEGIN
|
|
SELECT 1;
|
|
IF 1 = 1 THEN
|
|
SELECT 'ok';
|
|
END IF;
|
|
END;
|
|
SELECT 2;`;
|
|
|
|
const mysqlRoutineWithLoopsFixture = `CREATE PROCEDURE p_loop()
|
|
BEGIN
|
|
WHILE 1 = 0 DO
|
|
SELECT 'while; still body';
|
|
END WHILE;
|
|
REPEAT
|
|
SELECT 'repeat; still body';
|
|
UNTIL 1 = 1 END REPEAT;
|
|
END;
|
|
SELECT 2;`;
|
|
|
|
const mysqlDelimitedRoutineFixture = `DELIMITER //
|
|
CREATE PROCEDURE sp_insert_random_users(IN p_count INT)
|
|
BEGIN
|
|
DECLARE i INT DEFAULT 0;
|
|
DECLARE v_name VARCHAR(32);
|
|
|
|
WHILE i < p_count DO
|
|
SET v_name = CONCAT('user_', i);
|
|
INSERT INTO t_user (username) VALUES (v_name);
|
|
SET i = i + 1;
|
|
END WHILE;
|
|
END //
|
|
DELIMITER ;
|
|
|
|
CALL sp_insert_random_users(100);`;
|
|
|
|
const sapHanaDoBlockFixture = `DO
|
|
BEGIN
|
|
SELECT 1 AS "Result" FROM DUMMY;
|
|
END;
|
|
SELECT 2 FROM DUMMY;`;
|
|
|
|
describe("splitSqlStatementRanges", () => {
|
|
it("splits multiple top-level statements", () => {
|
|
const sql = "SELECT 1;\nSELECT 2;\nSELECT 3;";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2", "SELECT 3"]);
|
|
});
|
|
|
|
it("keeps a trailing statement without a semicolon", () => {
|
|
const sql = "SELECT 1;\nSELECT 2";
|
|
const ranges = splitSqlStatementRanges(sql);
|
|
expect(rangeSqlTexts(ranges)).toEqual(["SELECT 1", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons inside single-quoted strings", () => {
|
|
const sql = "INSERT INTO t VALUES ('a;b;c');\nSELECT 1";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["INSERT INTO t VALUES ('a;b;c')", "SELECT 1"]);
|
|
});
|
|
|
|
it("handles doubled single quotes as escaped quotes", () => {
|
|
const sql = "SELECT 'it''s; ok';\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 'it''s; ok'", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons inside double-quoted identifiers", () => {
|
|
const sql = 'SELECT "a;b";\nSELECT 2';
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(['SELECT "a;b"', "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons inside backtick identifiers (MySQL)", () => {
|
|
const sql = "SELECT `a;b`;\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT `a;b`", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons inside bracket identifiers (SQL Server)", () => {
|
|
const sql = "SELECT [a;b];\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT [a;b]", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons in line comments", () => {
|
|
const sql = "SELECT 1 -- a; b\n;\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons in hash line comments", () => {
|
|
const sql = "SELECT 1 # a; b\n;\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT 1", "SELECT 2"]);
|
|
});
|
|
|
|
it("keeps SQL Server temporary table names instead of treating them as hash comments", () => {
|
|
const sql = "DROP TABLE IF EXISTS #Temp;\nSELECT * FROM ##GlobalTemp;";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["DROP TABLE IF EXISTS #Temp", "SELECT * FROM ##GlobalTemp"]);
|
|
});
|
|
|
|
it("keeps MyBatis placeholders instead of treating them as hash comments", () => {
|
|
const sql = "SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym};\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "kingbase"))).toEqual(["SELECT * FROM yd_org_decla_detail WHERE clr_ym = #{ym}", "SELECT 2"]);
|
|
});
|
|
|
|
it("treats malformed or disabled MyBatis prefixes as hash comments", () => {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges("SELECT 1; #{1ym};\nSELECT 2", "kingbase"))).toEqual(["SELECT 1", "SELECT 2"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges("SELECT 1; #{ym};\nSELECT 2", "kingbase", { enabledSyntaxes: ["shell"] }))).toEqual(["SELECT 1", "SELECT 2"]);
|
|
});
|
|
|
|
it("ignores semicolons in block comments", () => {
|
|
const sql = "SELECT /* a; b */ 1;\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT /* a; b */ 1", "SELECT 2"]);
|
|
});
|
|
|
|
it("handles Postgres dollar quoting", () => {
|
|
const sql = "SELECT $$ a; b $$;\nSELECT 2";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql))).toEqual(["SELECT $$ a; b $$", "SELECT 2"]);
|
|
});
|
|
|
|
it("skips MySQL delimiter commands and empty custom delimiter statements", () => {
|
|
const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "mysql"))).toEqual(["select COUNT(1) FROM your_table", "select COUNT(1) FROM your_table;"]);
|
|
});
|
|
|
|
it("keeps MySQL routine blocks together without delimiter commands", () => {
|
|
const ranges = splitSqlStatementRanges(mysqlRoutineFixture, "mysql");
|
|
expect(rangeSqlTexts(ranges)).toEqual([mysqlRoutineFixture.slice(0, mysqlRoutineFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
|
expect(ranges[0].sql).toContain("SELECT 1;");
|
|
expect(ranges[0].sql).toContain("END IF;");
|
|
expect(ranges[0].sql).not.toMatch(/END;$/);
|
|
});
|
|
|
|
it("does not merge regular MySQL transaction statements as routine blocks", () => {
|
|
const sql = "BEGIN; INSERT INTO t VALUES (1); COMMIT;";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "mysql"))).toEqual(["BEGIN", "INSERT INTO t VALUES (1)", "COMMIT"]);
|
|
});
|
|
|
|
it("treats SQL Server GO lines as batch delimiters", () => {
|
|
const sql = "SELECT 1\nGO\nSELECT 2;\n GO 2\nSELECT 3";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["SELECT 1", "SELECT 2", "SELECT 3"]);
|
|
});
|
|
|
|
it("does not treat GO inside strings or comments as a SQL Server batch delimiter", () => {
|
|
const sql = "SELECT 'GO'\n-- GO\nSELECT 2\nGO\nSELECT 3";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "sqlserver"))).toEqual(["SELECT 'GO'\n-- GO\nSELECT 2", "SELECT 3"]);
|
|
});
|
|
|
|
it("keeps Oracle PL/SQL blocks together and treats slash lines as delimiters", () => {
|
|
const ranges = splitSqlStatementRanges(oraclePlSqlFixture, "oracle");
|
|
expect(rangeSqlTexts(ranges)).toEqual([oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")), "SELECT 1"]);
|
|
expect(ranges[0].sql).toContain("v_order_count NUMBER;");
|
|
expect(ranges[0].sql).toContain("END;");
|
|
expect(ranges[0].sql).not.toContain("\n/");
|
|
});
|
|
|
|
it("keeps issue #2405 Oracle PL/SQL block together without a slash delimiter", () => {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(oracleIssue2405PlSql, "oracle"))).toEqual([oracleIssue2405PlSql]);
|
|
});
|
|
|
|
it("splits large Dameng package bodies without repeated prefix parsing", () => {
|
|
const body = Array.from({ length: 3000 }, (_, index) => ` v_value := v_value + ${index % 10};`).join("\n");
|
|
const packageBody = `CREATE OR REPLACE PACKAGE BODY app.big_pkg AS
|
|
PROCEDURE run IS
|
|
v_value NUMBER := 0;
|
|
BEGIN
|
|
${body}
|
|
END run;
|
|
END big_pkg;`;
|
|
const sql = `${packageBody}\n/\nSELECT 1;`;
|
|
const startedAt = performance.now();
|
|
const ranges = splitSqlStatementRanges(sql, "dameng");
|
|
const elapsedMs = performance.now() - startedAt;
|
|
|
|
expect(rangeSqlTexts(ranges)).toEqual([packageBody, "SELECT 1"]);
|
|
expect(elapsedMs).toBeLessThan(1000);
|
|
});
|
|
|
|
it("keeps nested GaussDB procedure blocks together", () => {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbNestedProcedure, "gaussdb"))).toEqual([gaussDbNestedProcedure]);
|
|
});
|
|
|
|
it("separates GaussDB dollar-quoted functions from surrounding statements", () => {
|
|
const ranges = splitSqlStatementRanges(gaussDbDollarQuotedFunctionScript, "gaussdb");
|
|
expect(rangeSqlTexts(ranges)).toEqual([
|
|
"DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_md5_uuid",
|
|
gaussDbDollarQuotedFunctionScript.slice(gaussDbDollarQuotedFunctionScript.indexOf("CREATE"), gaussDbDollarQuotedFunctionScript.lastIndexOf(";\n\nDROP")),
|
|
"DROP FUNCTION IF EXISTS dbx_issue_4572_tmp_missing",
|
|
]);
|
|
});
|
|
|
|
it("separates the issue #4573 GaussDB procedure from following statements", () => {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(gaussDbIssue4573Script, "gaussdb"))).toEqual([gaussDbIssue4573Script.slice(0, gaussDbIssue4573Script.indexOf("\n\nSELECT 1")), "SELECT 1 AS after_procedure", "SELECT 2 AS final_statement"]);
|
|
});
|
|
|
|
it("keeps Xugu programmable object DDL together and retains its terminator", () => {
|
|
for (const sql of xuguProgrammableObjectFixtures) {
|
|
const ranges = splitSqlStatementRanges(`${sql}\nSELECT 1;`, "xugu");
|
|
expect(rangeSqlTexts(ranges)).toEqual([sql, "SELECT 1"]);
|
|
expect(ranges[0].sql.trimEnd()).toMatch(/END(?:\s+\w+)?;$/);
|
|
}
|
|
});
|
|
|
|
it("splits Xugu package specification without a slash before following SQL", () => {
|
|
const packageSpec = `CREATE OR REPLACE PACKAGE pkg_utils AS
|
|
FUNCTION get_version RETURN VARCHAR2;
|
|
PROCEDURE log_message(msg VARCHAR2);
|
|
END pkg_utils;`;
|
|
const forcePackageSpec = `CREATE OR REPLACE FORCE PACKAGE pkg_utils AS
|
|
PROCEDURE ping;
|
|
END pkg_utils;`;
|
|
const packageSpecWithoutReplace = `CREATE PACKAGE pkg_utils_without_replace AS
|
|
PROCEDURE ping;
|
|
END pkg_utils_without_replace;`;
|
|
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpec}\n/\nSELECT 1;`, "xugu"))).toEqual([packageSpec, "SELECT 1"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${forcePackageSpec}\nSELECT 1;`, "xugu"))).toEqual([forcePackageSpec, "SELECT 1"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${packageSpecWithoutReplace}\nSELECT 1;`, "xugu"))).toEqual([packageSpecWithoutReplace, "SELECT 1"]);
|
|
});
|
|
|
|
it("splits plain CREATE TYPE AS OBJECT on semicolon without waiting for END", () => {
|
|
const sql = "CREATE OR REPLACE TYPE address_t AS OBJECT (id INT);\nSELECT 1;";
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "xugu"))).toEqual(["CREATE OR REPLACE TYPE address_t AS OBJECT (id INT)", "SELECT 1"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges("CREATE TYPE address_t_without_replace AS OBJECT (id INT);\nSELECT 1;", "xugu"))).toEqual(["CREATE TYPE address_t_without_replace AS OBJECT (id INT)", "SELECT 1"]);
|
|
});
|
|
|
|
it("keeps Oracle-style CASE expressions inside Xugu and Oracle routines", () => {
|
|
const routine = `CREATE OR REPLACE FUNCTION dbx_case_expr RETURN NUMBER AS
|
|
BEGIN
|
|
RETURN CASE WHEN 1 = 1 THEN CASE WHEN 2 = 2 THEN 1 ELSE 2 END ELSE 0 END;
|
|
END;`;
|
|
const caseStatementRoutine = `CREATE OR REPLACE PROCEDURE dbx_case_statement AS
|
|
BEGIN
|
|
CASE WHEN 1 = 1 THEN NULL; ELSE NULL; END CASE;
|
|
END;`;
|
|
|
|
for (const database of ["xugu", "oracle"] as const) {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${routine}\nSELECT 1;`, database))).toEqual([routine, "SELECT 1"]);
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(`${caseStatementRoutine}\nSELECT 1;`, database))).toEqual([caseStatementRoutine, "SELECT 1"]);
|
|
}
|
|
});
|
|
|
|
it("keeps SAP HANA DO blocks together", () => {
|
|
const ranges = splitSqlStatementRanges(sapHanaDoBlockFixture, "saphana");
|
|
|
|
expect(rangeSqlTexts(ranges)).toEqual([sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")), "SELECT 2 FROM DUMMY"]);
|
|
expect(ranges[0].sql).toContain('SELECT 1 AS "Result" FROM DUMMY;');
|
|
expect(ranges[0].sql).toContain("END;");
|
|
});
|
|
});
|
|
|
|
describe("statementRangeAtCursor", () => {
|
|
it("splits Elasticsearch REST requests without semicolons", () => {
|
|
const sql = `# node information
|
|
GET /_nodes/stats/jvm?pretty
|
|
|
|
// search orders
|
|
POST /orders/_search
|
|
{
|
|
"query": { "match_all": {} }
|
|
}
|
|
|
|
HEAD /orders`;
|
|
|
|
for (const databaseType of ["elasticsearch", "easysearch"] as const) {
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, databaseType))).toEqual(["GET /_nodes/stats/jvm?pretty", 'POST /orders/_search\n{\n "query": { "match_all": {} }\n}', "HEAD /orders"]);
|
|
expect(hasMultipleExecutionTargets(sql, databaseType)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("targets the Elasticsearch request following a comment", () => {
|
|
const sql = "# JVM statistics\nGET /_nodes/stats/jvm?pretty\n\nGET /_cluster/health";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "JVM"), "elasticsearch")?.sql).toBe("GET /_nodes/stats/jvm?pretty");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "cluster"), "elasticsearch")?.sql).toBe("GET /_cluster/health");
|
|
});
|
|
|
|
it("splits and targets Elasticsearch requests after block comments", () => {
|
|
const sql = `/* node information
|
|
including JVM details */
|
|
GET /_nodes/stats/jvm?pretty
|
|
|
|
/* cluster information */
|
|
GET /_cluster/health`;
|
|
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "elasticsearch"))).toEqual(["GET /_nodes/stats/jvm?pretty", "GET /_cluster/health"]);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "JVM details"), "elasticsearch")?.sql).toBe("GET /_nodes/stats/jvm?pretty");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "cluster information"), "elasticsearch")?.sql).toBe("GET /_cluster/health");
|
|
});
|
|
|
|
it("ignores request-looking lines inside Elasticsearch block comments", () => {
|
|
const sql = `GET /_cluster/health
|
|
|
|
/* disabled cleanup
|
|
DELETE /important-index
|
|
*/
|
|
GET /_cat/indices`;
|
|
|
|
expect(rangeSqlTexts(splitSqlStatementRanges(sql, "elasticsearch"))).toEqual(["GET /_cluster/health", "GET /_cat/indices"]);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "important-index"), "elasticsearch")?.sql).toBe("GET /_cat/indices");
|
|
});
|
|
|
|
it("returns the first statement when the cursor is inside it", () => {
|
|
const sql = "SELECT 1;\nSELECT 2;";
|
|
const pos = indexOf(sql, "1");
|
|
const range = statementRangeAtCursor(sql, pos);
|
|
expect(range?.sql.trim()).toBe("SELECT 1");
|
|
});
|
|
|
|
it("returns the second statement when the cursor is inside it", () => {
|
|
const sql = "SELECT 1;\nSELECT 2;";
|
|
const pos = indexOf(sql, "2");
|
|
const range = statementRangeAtCursor(sql, pos);
|
|
expect(range?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("returns the statement when the cursor is in indentation before it", () => {
|
|
const sql = "SELECT 1;\n SELECT 2;";
|
|
const indentationPos = sql.indexOf(" SELECT 2") + 2;
|
|
const range = statementRangeAtCursor(sql, indentationPos);
|
|
expect(range?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("returns the previous statement when the cursor is in same-line whitespace after its semicolon", () => {
|
|
const sql = "SELECT 1; SELECT 2;";
|
|
const gapPos = sql.indexOf(";") + 2;
|
|
const range = statementRangeAtCursor(sql, gapPos);
|
|
expect(range?.sql.trim()).toBe("SELECT 1");
|
|
});
|
|
|
|
it("returns the previous statement when the cursor is just after its semicolon before a later statement", () => {
|
|
const sql = "SELECT *\nFROM system_dept;\n\nSELECT *\nFROM sys;";
|
|
const gapPos = sql.indexOf(";") + 1;
|
|
const range = statementRangeAtCursor(sql, gapPos);
|
|
expect(range?.sql.trim()).toBe("SELECT *\nFROM system_dept");
|
|
});
|
|
|
|
it("keeps a semicolon-line-end cursor on the current multi-line statement", () => {
|
|
const sql = "SELECT *\nFROM system_dept;";
|
|
const gapPos = sql.indexOf(";") + 1;
|
|
const range = statementRangeAtCursor(sql, gapPos);
|
|
expect(range?.sql.trim()).toBe("SELECT *\nFROM system_dept");
|
|
});
|
|
|
|
it("keeps a standalone next-line semicolon cursor on the current multi-line statement", () => {
|
|
const sql = "SELECT *\nFROM system_dept\n;\n\nSELECT * FROM sys;";
|
|
const delimiterPos = sql.indexOf(";");
|
|
|
|
expect(statementRangeAtCursor(sql, delimiterPos)?.sql.trim()).toBe("SELECT *\nFROM system_dept");
|
|
expect(statementRangeAtCursor(sql, delimiterPos + 1)?.sql.trim()).toBe("SELECT *\nFROM system_dept");
|
|
});
|
|
|
|
it("assigns a standalone trailing semicolon to the final soft statement", () => {
|
|
const sql = "SELECT * FROM `t_0001`\nSELECT * FROM `t_0001` LIMIT 1\n;";
|
|
const delimiterPos = sql.lastIndexOf(";");
|
|
|
|
expect(statementRangeAtCursor(sql, delimiterPos, "mysql")?.sql).toBe("SELECT * FROM `t_0001` LIMIT 1");
|
|
expect(statementRangeAtCursor(sql, delimiterPos + 1, "mysql")?.sql).toBe("SELECT * FROM `t_0001` LIMIT 1");
|
|
});
|
|
|
|
it("returns the next same-line statement when the cursor is inside it", () => {
|
|
const sql = "SELECT 1; SELECT 2;";
|
|
const pos = indexOf(sql, "SELECT 2") + 1;
|
|
const range = statementRangeAtCursor(sql, pos);
|
|
expect(range?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("returns a statement even without a trailing semicolon", () => {
|
|
const sql = "SELECT 1";
|
|
const pos = indexOf(sql, "1");
|
|
const range = statementRangeAtCursor(sql, pos);
|
|
expect(range?.sql.trim()).toBe("SELECT 1");
|
|
});
|
|
|
|
it("stops at the next top-level statement start when the cursor statement has no semicolon", () => {
|
|
const sql = "SELECT 1\nSELECT 2;\nSELECT 3;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "1"));
|
|
expect(range?.sql.trim()).toBe("SELECT 1");
|
|
});
|
|
|
|
it("returns the later top-level statement when earlier statements are missing semicolons", () => {
|
|
const sql = "SELECT 1\nSELECT 2;\nSELECT 3;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "2"));
|
|
expect(range?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("keeps newline set-operation SELECT operands with the cursor statement", () => {
|
|
const sql = "select * from tbA\nunion\nselect * from tbB";
|
|
const expected = "select * from tbA\nunion\nselect * from tbB";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "tbA"))?.sql.trim()).toBe(expected);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "tbB"))?.sql.trim()).toBe(expected);
|
|
});
|
|
|
|
it("keeps newline set-operation operands with ALL modifiers together", () => {
|
|
const sql = "select * from tbA\nunion all\nselect * from tbB\nSELECT * FROM logs;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "tbA"));
|
|
|
|
expect(range?.sql.trim()).toBe("select * from tbA\nunion all\nselect * from tbB");
|
|
});
|
|
|
|
it("keeps a multi-line select together when continuation lines do not start statements", () => {
|
|
const sql = "SELECT id,\n name\nFROM users\nWHERE active = 1\nSELECT * FROM logs;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "name"));
|
|
expect(range?.sql.trim()).toBe("SELECT id,\n name\nFROM users\nWHERE active = 1");
|
|
});
|
|
|
|
it("keeps a CTE main query with its WITH statement", () => {
|
|
const sql = "WITH active_users AS (\n SELECT * FROM users\n)\nSELECT * FROM active_users\nSELECT * FROM logs;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "active_users", 2));
|
|
expect(range?.sql.trim()).toBe("WITH active_users AS (\n SELECT * FROM users\n)\nSELECT * FROM active_users");
|
|
});
|
|
|
|
it("keeps update assignments with the UPDATE statement", () => {
|
|
const sql = "UPDATE users\nSET name = 'a'\nWHERE id = 1\nSELECT * FROM users;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "name"));
|
|
expect(range?.sql.trim()).toBe("UPDATE users\nSET name = 'a'\nWHERE id = 1");
|
|
});
|
|
|
|
it("keeps MySQL ALTER TABLE column comments with the column definition", () => {
|
|
const sql =
|
|
"ALTER TABLE `yb_course_order`\n ADD COLUMN `audit_status` tinyint(4) DEFAULT NULL\n COMMENT '审核状态:0-待审核,1-已通过,2-已拒绝',\n ADD COLUMN `close_reason` varchar(30) DEFAULT NULL\n COMMENT '关闭原因:timeout-超时关闭,cancel-取消关闭,refund-退款关闭',\n ADD COLUMN `paid_completion_time` datetime DEFAULT NULL\n COMMENT '订单完成支付(付清)时间 首次全额支付完成时记录,全部退款后不重置';";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "close_reason"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL CREATE TABLE options with table comments", () => {
|
|
const sql = `CREATE TABLE test_1 (
|
|
id bigint NOT NULL AUTO_INCREMENT COMMENT '主键id',
|
|
deleted tinyint NOT NULL DEFAULT 0 COMMENT '删除标志(0:有效 1:无效)',
|
|
locked tinyint NOT NULL DEFAULT 0 COMMENT '是否锁定(0.否,1.是)',
|
|
version int NOT NULL DEFAULT 0 COMMENT '版本号',
|
|
creatorId bigint DEFAULT NULL COMMENT '创建人ID',
|
|
createBy varchar(100) DEFAULT NULL COMMENT '创建人名称',
|
|
createdTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
|
updaterId bigint DEFAULT NULL COMMENT '修改人ID',
|
|
updatedBy varchar(100) DEFAULT NULL COMMENT '修改人名称',
|
|
updatedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
|
|
PRIMARY KEY (id)
|
|
)
|
|
ENGINE = INNODB,
|
|
CHARACTER SET utf8mb4,
|
|
COLLATE utf8mb4_general_ci,
|
|
COMMENT = '测试';`;
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "CREATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT ="), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT ="))?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL CREATE TABLE comments without equals as table options", () => {
|
|
const sql = "CREATE TABLE test_2 (\n id bigint NOT NULL\n)\nCOMMENT '测试';";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "COMMENT"))?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("does not merge standard COMMENT ON statements into preceding CREATE TABLE statements", () => {
|
|
const sql = "CREATE TABLE users (id int)\nCOMMENT ON TABLE users IS 'Users';";
|
|
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual(["CREATE TABLE users (id int)", "COMMENT ON TABLE users IS 'Users'"]);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual(["CREATE TABLE users (id int)", "COMMENT ON TABLE users IS 'Users'"]);
|
|
});
|
|
|
|
it("keeps a line-start comment column inside a select projection", () => {
|
|
const sql = "SELECT\n id,\n comment,\n created_at\nFROM project_info\nWHERE deleted = 0;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "comment"))?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL ALTER TABLE drop column clauses with the statement", () => {
|
|
const sql = "ALTER TABLE t\n DROP COLUMN a,\n DROP COLUMN b;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "DROP COLUMN b"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL ALTER TABLE alter column clauses with the statement", () => {
|
|
const sql = "ALTER TABLE t\n ALTER COLUMN name SET NOT NULL;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER COLUMN"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL ALTER TABLE truncate partition clauses with the statement", () => {
|
|
const sql = "ALTER TABLE ems.r_r_curve_e_hour\nTRUNCATE PARTITION p202602, p202603, p202604, p202605, p202606;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL truncate partition clauses when comments separate ALTER TABLE", () => {
|
|
const sql = "ALTER /* online ddl */ TABLE t\nTRUNCATE PARTITION p0;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL truncate partition clauses when comments precede PARTITION", () => {
|
|
const sql = "ALTER TABLE t\nTRUNCATE /* keep */ PARTITION p0;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps standalone truncate table statements separate from preceding alter statements", () => {
|
|
const sql = "ALTER TABLE events ADD COLUMN source varchar(100)\nTRUNCATE TABLE events;";
|
|
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual(["ALTER TABLE events ADD COLUMN source varchar(100)", "TRUNCATE TABLE events"]);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual(["ALTER TABLE events ADD COLUMN source varchar(100)", "TRUNCATE TABLE events"]);
|
|
});
|
|
|
|
it("keeps issue #4045 ClickHouse ALTER TABLE UPDATE mutation together", () => {
|
|
const sql = `ALTER TABLE m2_db.history_data5
|
|
UPDATE value = 14.06
|
|
WHERE uuid IN (
|
|
SELECT uuid
|
|
FROM m2_db.pt_uuid_mapping
|
|
WHERE point_id = 'TEST_Location_1_L2_METER_2_AE_DIFF_H'
|
|
)
|
|
AND ts = toDateTime('2026-07-15 12:00:00');`;
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "UPDATE"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps a simple ClickHouse ALTER TABLE UPDATE mutation together", () => {
|
|
const sql = "ALTER TABLE `events` ON CLUSTER 'analytics'\nUPDATE value = 1\nWHERE id = 42;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "UPDATE"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("separates statements after a ClickHouse ALTER TABLE UPDATE mutation", () => {
|
|
const mutation = "ALTER TABLE events\nUPDATE value = 1\nWHERE id = 42";
|
|
const sql = `${mutation};\nSELECT * FROM events;\nUPDATE audit SET processed = 1;`;
|
|
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([mutation, "SELECT * FROM events", "UPDATE audit SET processed = 1"]);
|
|
});
|
|
|
|
it("does not merge independent UPDATE statements into other ALTER statements", () => {
|
|
const otherDatabaseSql = "ALTER TABLE events\nUPDATE audit SET processed = 1;";
|
|
const clickHouseSql = "ALTER TABLE events ADD COLUMN source String\nUPDATE audit SET processed = 1;";
|
|
|
|
expect(rangeSqlTexts(executableStatementRanges(otherDatabaseSql, "postgres"))).toEqual(["ALTER TABLE events", "UPDATE audit SET processed = 1"]);
|
|
expect(rangeSqlTexts(executableStatementRanges(otherDatabaseSql, "mysql"))).toEqual(["ALTER TABLE events", "UPDATE audit SET processed = 1"]);
|
|
expect(rangeSqlTexts(executableStatementRanges(clickHouseSql, "clickhouse"))).toEqual(["ALTER TABLE events ADD COLUMN source String", "UPDATE audit SET processed = 1"]);
|
|
});
|
|
|
|
it("keeps insert-select with the INSERT statement", () => {
|
|
const sql = "INSERT INTO archived_users (id, name)\nSELECT id, name FROM users\nUPDATE users SET archived = 1;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "archived_users"));
|
|
expect(range?.sql.trim()).toBe("INSERT INTO archived_users (id, name)\nSELECT id, name FROM users");
|
|
});
|
|
|
|
it("keeps explain target SQL with the EXPLAIN statement", () => {
|
|
const sql = "EXPLAIN\nSELECT * FROM users\nSELECT * FROM logs;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "EXPLAIN"));
|
|
expect(range?.sql.trim()).toBe("EXPLAIN\nSELECT * FROM users");
|
|
});
|
|
|
|
it("keeps issue #3567 EXPLAIN options and CTE target as one statement", () => {
|
|
const sql = "explain (analyze,buffers)\nwith tmp as(select* from test.tt)\nselect * from tmp;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "explain"), "postgres")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "select * from tmp"), "postgres")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([sql.slice(0, -1)]);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps EXPLAIN ANALYZE with a CTE main query as one statement", () => {
|
|
const sql = "explain analyze\nwith tmp as (select 1)\nselect * from tmp;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "explain"), "postgres")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps a plain EXPLAIN CTE target as one statement without merging later queries", () => {
|
|
const sql = "EXPLAIN\nWITH tmp AS (SELECT 1)\nSELECT * FROM tmp\nSELECT * FROM logs;";
|
|
const expected = "EXPLAIN\nWITH tmp AS (SELECT 1)\nSELECT * FROM tmp";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "EXPLAIN"))?.sql.trim()).toBe(expected);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([expected, "SELECT * FROM logs"]);
|
|
});
|
|
|
|
it("does not merge a query after an inline EXPLAIN options CTE statement", () => {
|
|
const sql = "EXPLAIN (ANALYZE) WITH tmp AS (SELECT 1)\nSELECT * FROM tmp\nSELECT 2;";
|
|
const expected = "EXPLAIN (ANALYZE) WITH tmp AS (SELECT 1)\nSELECT * FROM tmp";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "EXPLAIN"), "postgres")?.sql.trim()).toBe(expected);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([expected, "SELECT 2"]);
|
|
});
|
|
|
|
it("keeps EXPLAIN options CTE UPDATE assignments as one statement", () => {
|
|
const sql = "EXPLAIN (ANALYZE)\nWITH tmp AS (SELECT 1)\nUPDATE t\nSET x = 1;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "SET"), "postgres")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps CTE INSERT ... SELECT with the WITH statement", () => {
|
|
const sql = "WITH tmp AS (SELECT 1)\nINSERT INTO t (id)\nSELECT * FROM tmp;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "INSERT"))?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("does not merge a query after a CTE INSERT ... SELECT statement", () => {
|
|
const sql = "WITH tmp AS (SELECT 1)\nINSERT INTO t (id)\nSELECT * FROM tmp\nSELECT 2;";
|
|
const expected = "WITH tmp AS (SELECT 1)\nINSERT INTO t (id)\nSELECT * FROM tmp";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "INSERT"))?.sql.trim()).toBe(expected);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([expected, "SELECT 2"]);
|
|
});
|
|
|
|
it("does not merge a query after an INSERT with a CTE source query", () => {
|
|
const sql = "INSERT INTO t (id)\nWITH tmp AS (SELECT 1)\nSELECT * FROM tmp\nSELECT 2;";
|
|
const expected = "INSERT INTO t (id)\nWITH tmp AS (SELECT 1)\nSELECT * FROM tmp";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "INSERT"))?.sql.trim()).toBe(expected);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([expected, "SELECT 2"]);
|
|
});
|
|
|
|
it("skips block comments inside EXPLAIN options when resolving the target", () => {
|
|
const sql = "EXPLAIN (ANALYZE /* ) */) UPDATE t\nSET x = 1;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "SET"), "postgres")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("recovers later statements from an unclosed EXPLAIN option list", () => {
|
|
const sql = "EXPLAIN (ANALYZE\nSELECT 1\nSELECT 2;";
|
|
const explainSql = "EXPLAIN (ANALYZE\nSELECT 1";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "EXPLAIN"), "postgres")?.sql.trim()).toBe(explainSql);
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "SELECT 2"), "postgres")?.sql.trim()).toBe("SELECT 2");
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual([explainSql, "SELECT 2"]);
|
|
});
|
|
|
|
it("does not treat a DESCRIBE subquery paren as EXPLAIN options", () => {
|
|
const sql = "DESCRIBE (SELECT 1)\nSELECT 2;";
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "DESCRIBE"), "clickhouse")?.sql.trim()).toBe("DESCRIBE (SELECT 1)");
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual(["DESCRIBE (SELECT 1)", "SELECT 2"]);
|
|
expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual(["DESCRIBE (SELECT 1)", "SELECT 2"]);
|
|
});
|
|
|
|
it("keeps MySQL DESC UPDATE joins as one statement", () => {
|
|
const sql = "desc update test_orders a\njoin test_users b\non a.id=b.id \nset a.name = '张三'\nwhere b.id > 10;";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "desc"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "set"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL EXPLAIN UPDATE assignments as one statement", () => {
|
|
const sql = "EXPLAIN UPDATE test_orders a\nJOIN test_users b ON a.id=b.id\nSET a.name = '张三'\nWHERE b.id > 10;";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "SET"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("keeps MySQL REPLACE function calls inside UPDATE assignments", () => {
|
|
const sql = `UPDATE ecm_archive_prepare_pool
|
|
SET
|
|
request_json =
|
|
REPLACE(
|
|
request_json,
|
|
'"paperFlag":null',
|
|
'"paperFlag":false'
|
|
),
|
|
process_flag = 0
|
|
WHERE request_json LIKE '%"paperFlag":null%';`;
|
|
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "REPLACE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1));
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
|
|
});
|
|
|
|
it("does not merge a plain MySQL DESC table statement with the next query", () => {
|
|
const sql = "DESC users\nSELECT * FROM users;";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "DESC"), "mysql")?.sql.trim()).toBe("DESC users");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "SELECT"), "mysql")?.sql.trim()).toBe("SELECT * FROM users");
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual(["DESC users", "SELECT * FROM users"]);
|
|
});
|
|
|
|
it("does not include comments between soft statement blocks", () => {
|
|
const sql = "SELECT 1\n-- explain the next query\n/* still next query notes */\nSELECT 2;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "1"));
|
|
expect(range?.sql.trim()).toBe("SELECT 1");
|
|
});
|
|
|
|
it("detects a soft statement start after a leading block comment on the same line", () => {
|
|
const sql = "SELECT 1\n/* next */ SELECT 2;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "2"));
|
|
expect(range?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("uses database-specific soft statement keywords", () => {
|
|
const sql = "SELECT 1\nDO $$ BEGIN RAISE NOTICE 'x'; END $$;";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "1"))?.sql.trim()).toBe("SELECT 1\nDO $$ BEGIN RAISE NOTICE 'x'; END $$");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "1"), "postgres")?.sql.trim()).toBe("SELECT 1");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "DO"), "postgres")?.sql.trim()).toBe("DO $$ BEGIN RAISE NOTICE 'x'; END $$");
|
|
});
|
|
|
|
it("returns null when the cursor is on a blank line", () => {
|
|
const sql = "SELECT 1;\n\nSELECT 2;";
|
|
const blankLinePos = sql.indexOf("\n") + 1;
|
|
expect(statementRangeAtCursor(sql, blankLinePos)).toBeNull();
|
|
});
|
|
|
|
it("returns null for an empty document", () => {
|
|
expect(statementRangeAtCursor("", 0)).toBeNull();
|
|
});
|
|
|
|
it("does not treat comment semicolons as delimiters", () => {
|
|
const sql = "SELECT 1; -- drop; this\nSELECT 2;";
|
|
const pos = indexOf(sql, "2");
|
|
expect(statementRangeAtCursor(sql, pos)?.sql.trim()).toBe("SELECT 2");
|
|
});
|
|
|
|
it("exposes offsets aligned to the statement body", () => {
|
|
const sql = " SELECT 1;\nSELECT 2;";
|
|
const range = statementRangeAtCursor(sql, indexOf(sql, "1"));
|
|
expect(range?.from).toBe(2);
|
|
expect(range?.sql).toBe("SELECT 1");
|
|
});
|
|
|
|
it("skips MySQL delimiter commands when resolving the cursor statement", () => {
|
|
const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "COUNT", 2), "mysql")?.sql.trim()).toBe("select COUNT(1) FROM your_table;");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "delimiter"), "mysql")).toBeNull();
|
|
});
|
|
|
|
it("returns the full MySQL routine block for cursors inside nested statements", () => {
|
|
const range = statementRangeAtCursor(mysqlRoutineFixture, indexOf(mysqlRoutineFixture, "ok"), "mysql");
|
|
expect(range?.sql.trim()).toBe(mysqlRoutineFixture.slice(0, mysqlRoutineFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim());
|
|
});
|
|
|
|
it("returns null on SQL Server GO batch delimiter lines", () => {
|
|
const sql = "SELECT 1\nGO\nSELECT 2";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "GO"), "sqlserver")).toBeNull();
|
|
});
|
|
|
|
it("returns the current SQL Server batch around GO delimiters", () => {
|
|
const sql = "SELECT 1\nGO\nSELECT 2\nGO\nSELECT 3";
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "1"), "sqlserver")?.sql.trim()).toBe("SELECT 1");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "2"), "sqlserver")?.sql.trim()).toBe("SELECT 2");
|
|
expect(statementRangeAtCursor(sql, indexOf(sql, "3"), "sqlserver")?.sql.trim()).toBe("SELECT 3");
|
|
});
|
|
|
|
it("returns the full Oracle PL/SQL block for cursors inside nested statements", () => {
|
|
const range = statementRangeAtCursor(oraclePlSqlFixture, indexOf(oraclePlSqlFixture, "ORDERS_10K", 2), "oracle");
|
|
expect(range?.sql.trim()).toBe(oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")));
|
|
});
|
|
|
|
it("returns the full issue #2405 Oracle PL/SQL block for cursors inside the block", () => {
|
|
for (const cursor of [indexOf(oracleIssue2405PlSql, "PRE_TRD_DATE"), indexOf(oracleIssue2405PlSql, "SELECT 1 + 2"), indexOf(oracleIssue2405PlSql, "END;")]) {
|
|
expect(statementRangeAtCursor(oracleIssue2405PlSql, cursor, "oracle")?.sql.trim()).toBe(oracleIssue2405PlSql);
|
|
}
|
|
});
|
|
|
|
it("returns the full GaussDB procedure for cursors after a nested block", () => {
|
|
const outerNull = indexOf(gaussDbNestedProcedure, "NULL;", 2);
|
|
expect(statementRangeAtCursor(gaussDbNestedProcedure, outerNull, "gaussdb")?.sql.trim()).toBe(gaussDbNestedProcedure);
|
|
});
|
|
|
|
it("returns only the issue #4573 GaussDB procedure for a gutter cursor", () => {
|
|
const expected = gaussDbIssue4573Script.slice(0, gaussDbIssue4573Script.indexOf("\n\nSELECT 1"));
|
|
expect(statementRangeAtCursor(gaussDbIssue4573Script, indexOf(gaussDbIssue4573Script, "count(*)"), "gaussdb")?.sql.trim()).toBe(expected);
|
|
});
|
|
|
|
it("returns the full SAP HANA DO block for cursors inside nested statements", () => {
|
|
const range = statementRangeAtCursor(sapHanaDoBlockFixture, indexOf(sapHanaDoBlockFixture, "Result"), "saphana");
|
|
|
|
expect(range?.sql.trim()).toBe(sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")));
|
|
});
|
|
});
|
|
|
|
describe("executableStatementRanges", () => {
|
|
it("returns statement ranges starting only at statement starts", () => {
|
|
const sql = "SELECT *\nFROM users\nWHERE active = 1;\nSELECT 2;";
|
|
const ranges = executableStatementRanges(sql);
|
|
expect(rangeSqlTexts(ranges)).toEqual(["SELECT *\nFROM users\nWHERE active = 1", "SELECT 2"]);
|
|
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("SELECT 2")]);
|
|
});
|
|
|
|
it("returns Redis executable command lines", () => {
|
|
const sql = "GET user:1\n# comment\n DEL user:2 ";
|
|
const ranges = executableStatementRanges(sql, "redis");
|
|
expect(rangeSqlTexts(ranges)).toEqual(["GET user:1", "DEL user:2"]);
|
|
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("DEL")]);
|
|
});
|
|
|
|
it("keeps MySQL REPLACE INTO as an executable statement start", () => {
|
|
const sql = "SELECT 1\nREPLACE INTO users (id, name) VALUES (1, 'a');";
|
|
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual(["SELECT 1", "REPLACE INTO users (id, name) VALUES (1, 'a')"]);
|
|
});
|
|
|
|
it("returns MongoDB command ranges for newline-separated shell commands", () => {
|
|
const sql = 'use archive\ndb.users.find({ status: "open" })\n .limit(5)';
|
|
const ranges = executableStatementRanges(sql, "mongodb");
|
|
|
|
expect(rangeSqlTexts(ranges)).toEqual(["use archive", 'db.users.find({ status: "open" })\n .limit(5)']);
|
|
expect(ranges.map((range) => range.from)).toEqual([0, sql.indexOf("db.users.find")]);
|
|
});
|
|
|
|
it("does not split executable Oracle PL/SQL ranges at inner statement starts", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(oraclePlSqlFixture, "oracle"))).toEqual([oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")), "SELECT 1"]);
|
|
});
|
|
|
|
it("returns the issue #2405 Oracle PL/SQL block as one executable range", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(oracleIssue2405PlSql, "oracle"))).toEqual([oracleIssue2405PlSql]);
|
|
});
|
|
|
|
it("does not split executable MySQL routine ranges at inner statements", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(mysqlRoutineFixture, "mysql"))).toEqual([mysqlRoutineFixture.slice(0, mysqlRoutineFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
|
});
|
|
|
|
it("does not split executable MySQL routine ranges at WHILE and REPEAT endings", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(mysqlRoutineWithLoopsFixture, "mysql"))).toEqual([mysqlRoutineWithLoopsFixture.slice(0, mysqlRoutineWithLoopsFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
|
});
|
|
|
|
it("does not expose run targets for statements inside a delimited MySQL routine", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(mysqlDelimitedRoutineFixture, "mysql"))).toEqual([mysqlDelimitedRoutineFixture.slice(mysqlDelimitedRoutineFixture.indexOf("CREATE PROCEDURE"), mysqlDelimitedRoutineFixture.indexOf(" //\nDELIMITER")), "CALL sp_insert_random_users(100)"]);
|
|
});
|
|
|
|
it("does not split executable SAP HANA DO ranges at inner statements", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges(sapHanaDoBlockFixture, "saphana"))).toEqual([sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")), "SELECT 2 FROM DUMMY"]);
|
|
});
|
|
|
|
it("returns executable SQL Server batches without GO delimiter lines", () => {
|
|
expect(rangeSqlTexts(executableStatementRanges("SELECT 1\nGO\nSELECT 2;", "sqlserver"))).toEqual(["SELECT 1", "SELECT 2"]);
|
|
});
|
|
});
|
|
|
|
describe("currentExecutableStatementRange", () => {
|
|
it("uses the current SQL statement range for multi-line DDL", () => {
|
|
const sql = "ALTER TABLE `yb_course_order`\n ADD COLUMN `audit_status` tinyint(4) DEFAULT NULL\n COMMENT '审核状态:0-待审核,1-已通过,2-已拒绝',\n ADD COLUMN `close_reason` varchar(30) DEFAULT NULL\n COMMENT '关闭原因:timeout-超时关闭,cancel-取消关闭,refund-退款关闭';\nSELECT 1;";
|
|
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "close_reason"), "mysql")?.sql.trim()).toBe(sql.slice(0, sql.indexOf(";\nSELECT")));
|
|
});
|
|
|
|
it("returns null on blank and pure comment lines", () => {
|
|
const sql = "SELECT 1;\n-- comment\n\nSELECT 2;";
|
|
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "comment"), "mysql")).toBeNull();
|
|
expect(currentExecutableStatementRange(sql, sql.indexOf("\n\n") + 1, "mysql")).toBeNull();
|
|
});
|
|
|
|
it("uses the current Redis command line", () => {
|
|
const sql = "GET user:1\n DEL user:2\n# comment";
|
|
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "DEL"), "redis")?.sql).toBe("DEL user:2");
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "comment"), "redis")).toBeNull();
|
|
});
|
|
|
|
it("uses the current MongoDB command range", () => {
|
|
const sql = 'db.users.find({})\n\ndb.getCollection("audit.logs").countDocuments({})';
|
|
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "users"), "mongodb")?.sql).toBe("db.users.find({})");
|
|
expect(currentExecutableStatementRange(sql, indexOf(sql, "audit.logs"), "mongodb")?.sql).toBe('db.getCollection("audit.logs").countDocuments({})');
|
|
});
|
|
});
|
|
|
|
describe("fullSqlRange", () => {
|
|
it("returns the trimmed full document", () => {
|
|
const sql = " SELECT 1; \n";
|
|
const range = fullSqlRange(sql);
|
|
expect(range?.sql).toBe("SELECT 1;");
|
|
});
|
|
|
|
it("returns null for an empty/whitespace document", () => {
|
|
expect(fullSqlRange(" \n ")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("buildExecutionCandidates", () => {
|
|
it("returns a single candidate when only the cursor statement exists", () => {
|
|
const sql = "SELECT 1";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "1"));
|
|
expect(candidates).toHaveLength(1);
|
|
expect(candidates[0].kind).toBe("all");
|
|
});
|
|
|
|
it("returns current + all in order for multiple statements", () => {
|
|
const sql = "SELECT 1;\nSELECT 2;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "2"));
|
|
expect(candidateKinds(candidates)).toEqual(["cursor", "all"]);
|
|
});
|
|
|
|
it("preserves leading optimizer hints in current statement candidates", () => {
|
|
const hintedSql = "/*+ SET(polar_csi.enable_query on) SET(polar_csi.cost_threshold 0)*/\nselect count(1) from xxx";
|
|
const sql = `select 1;\n${hintedSql};`;
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "count"), "postgres");
|
|
|
|
expect(candidates[0].sql).toBe(hintedSql);
|
|
});
|
|
|
|
it("uses the cursor statement for the first candidate when there is no selection", () => {
|
|
const sql = "SELECT *\nFROM users\nWHERE active = 1";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "users"));
|
|
expect(candidates).toHaveLength(1);
|
|
expect(candidates[0].kind).toBe("all");
|
|
});
|
|
|
|
it("uses the whole set-operation statement for cursor execution candidates", () => {
|
|
const sql = "select * from tbA\nunion\nselect * from tbB\nSELECT * FROM logs;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "tbA"));
|
|
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:select * from tbA\nunion\nselect * from tbB", "all:select * from tbA\nunion\nselect * from tbB\nSELECT * FROM logs;"]);
|
|
});
|
|
|
|
it("uses the current command line for Redis cursor candidates", () => {
|
|
const sql = "GET user:1\nDEL user:2\nHGETALL user:3";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "user:2"), "redis");
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:DEL user:2", "all:GET user:1\nDEL user:2\nHGETALL user:3"]);
|
|
expect(candidateLabels(candidates)).toEqual(["currentCommand", "allCommands"]);
|
|
});
|
|
|
|
it("returns only all for Redis when the cursor is on a comment line", () => {
|
|
const sql = "GET user:1\n# comment\nDEL user:2";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "comment"), "redis");
|
|
expect(candidateSummaries(candidates)).toEqual(["all:GET user:1\n# comment\nDEL user:2"]);
|
|
});
|
|
|
|
it("returns current + all when the cursor is in indentation before a statement", () => {
|
|
const sql = "SELECT 1;\n SELECT 2;";
|
|
const indentationPos = sql.indexOf(" SELECT 2") + 2;
|
|
const candidates = buildExecutionCandidates(sql, indentationPos);
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT 2", "all:SELECT 1;\n SELECT 2;"]);
|
|
expect(candidateLabels(candidates)).toEqual(["currentStatement", "allStatements"]);
|
|
});
|
|
|
|
it("keeps a MySQL FORCE INDEX query current when its semicolon is on the next line", () => {
|
|
const firstStatement = `SELECT count(*)
|
|
FROM cus_loan_status_copy1 t2
|
|
LEFT JOIN case_allocation_details_copy1 t4 FORCE INDEX(idx_debt_case_number)
|
|
ON t2.caseno = t4.debt_case_number
|
|
WHERE t2.product_name = '12345'
|
|
;`;
|
|
const sql = `${firstStatement}\n\nSELECT count(*) FROM cus_loan_status_copy1;`;
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "12345"), "mysql");
|
|
|
|
expect(candidateKinds(candidates)).toEqual(["cursor", "all"]);
|
|
expect(executionCandidateForMode(candidates, "current")?.sql.trim()).toBe(firstStatement.replace(/\n;$/, ""));
|
|
});
|
|
|
|
it("uses the current statement when the cursor is immediately after its semicolon before a blank line", () => {
|
|
const sql = "select 1;\n\nselect 2;";
|
|
const cursorAfterFirstSemicolon = sql.indexOf(";") + 1;
|
|
const candidates = buildExecutionCandidates(sql, cursorAfterFirstSemicolon);
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:select 1", "all:select 1;\n\nselect 2;"]);
|
|
});
|
|
|
|
it("uses the final soft statement when the cursor follows trailing EOF whitespace", () => {
|
|
const sql = "SELECT 1\nSELECT 2 ";
|
|
const candidates = buildExecutionCandidates(sql, sql.length);
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT 2", "all:SELECT 1\nSELECT 2"]);
|
|
});
|
|
|
|
it("uses the final soft statement when the cursor is on a standalone trailing semicolon", () => {
|
|
const sql = "SELECT * FROM `t_0001`\nSELECT * FROM `t_0001` LIMIT 1\n;";
|
|
const candidates = buildExecutionCandidates(sql, sql.lastIndexOf(";"), "mysql");
|
|
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT * FROM `t_0001` LIMIT 1", "all:SELECT * FROM `t_0001`\nSELECT * FROM `t_0001` LIMIT 1\n;"]);
|
|
});
|
|
|
|
it("dedupes when the cursor statement equals the full document", () => {
|
|
const sql = "SELECT 1;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "1"));
|
|
expect(candidates).toHaveLength(1);
|
|
expect(candidates[0].kind).toBe("all");
|
|
});
|
|
|
|
it("returns only 'all' when the cursor is on a blank line", () => {
|
|
const sql = "SELECT 1;\n\nSELECT 2;";
|
|
const candidates = buildExecutionCandidates(sql, sql.indexOf("\n") + 1);
|
|
expect(candidateKinds(candidates)).toEqual(["all"]);
|
|
expect(candidates[0].supportedKinds).toEqual(["all"]);
|
|
expect(executionCandidateForMode(candidates, "current")).toBeNull();
|
|
expect(executionCandidateForMode(candidates, "all")).toBe(candidates[0]);
|
|
});
|
|
|
|
it("marks a deduplicated single-statement candidate as both current and all", () => {
|
|
const sql = "SELECT 1;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "1"));
|
|
|
|
expect(candidates[0].supportedKinds).toEqual(["cursor", "all"]);
|
|
});
|
|
|
|
it("returns no candidates for an empty document", () => {
|
|
expect(buildExecutionCandidates("", 0)).toEqual([]);
|
|
});
|
|
|
|
it("returns only 'all' when the cursor has no statement but the document has SQL", () => {
|
|
// Cursor past the end on a trailing blank line.
|
|
const sql = "SELECT 1;\nSELECT 2;\n";
|
|
const candidates = buildExecutionCandidates(sql, sql.length);
|
|
expect(candidateKinds(candidates)).toEqual(["all"]);
|
|
});
|
|
|
|
it("uses the MySQL statement body for delimiter scripts", () => {
|
|
const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "COUNT", 2), "mysql");
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:select COUNT(1) FROM your_table;", "all:select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"]);
|
|
});
|
|
|
|
it("uses the current SQL Server batch for cursor candidates", () => {
|
|
const sql = "SELECT 1\nGO\nSELECT 2;";
|
|
const candidates = buildExecutionCandidates(sql, indexOf(sql, "2"), "sqlserver");
|
|
expect(candidateSummaries(candidates)).toEqual(["cursor:SELECT 2", "all:SELECT 1\nGO\nSELECT 2;"]);
|
|
});
|
|
});
|
|
|
|
describe("hasMultipleExecutionTargets", () => {
|
|
it("returns false for a single SQL statement", () => {
|
|
expect(hasMultipleExecutionTargets("SELECT 1;")).toBe(false);
|
|
});
|
|
|
|
it("returns true for multiple SQL statements", () => {
|
|
expect(hasMultipleExecutionTargets("SELECT 1;\nSELECT 2;")).toBe(true);
|
|
});
|
|
|
|
it("ignores comments when counting SQL statements", () => {
|
|
expect(hasMultipleExecutionTargets("-- check one thing\nSELECT 1;")).toBe(false);
|
|
});
|
|
|
|
it("counts executable Redis command lines", () => {
|
|
expect(hasMultipleExecutionTargets("GET user:1", "redis")).toBe(false);
|
|
expect(hasMultipleExecutionTargets("GET user:1\n# comment\nDEL user:2", "redis")).toBe(true);
|
|
});
|
|
|
|
it("counts MySQL delimiter scripts by executable statements", () => {
|
|
const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;";
|
|
expect(hasMultipleExecutionTargets(sql, "mysql")).toBe(true);
|
|
});
|
|
|
|
it("counts MySQL routine blocks without delimiter by executable statements", () => {
|
|
expect(hasMultipleExecutionTargets(mysqlRoutineFixture, "mysql")).toBe(true);
|
|
});
|
|
|
|
it("counts SQL Server GO batches as multiple execution targets", () => {
|
|
expect(hasMultipleExecutionTargets("SELECT 1\nGO\nSELECT 2", "sqlserver")).toBe(true);
|
|
});
|
|
|
|
it("does not show multiple targets for MySQL DESC UPDATE joins", () => {
|
|
const sql = "desc update test_orders a\njoin test_users b\non a.id=b.id \nset a.name = '张三'\nwhere b.id > 10;";
|
|
expect(hasMultipleExecutionTargets(sql, "mysql")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("supportsExecutionTargetPicker", () => {
|
|
it("enables the picker for SQL database connections, Redis, and Elasticsearch", () => {
|
|
expect(supportsExecutionTargetPicker("mysql")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("postgres")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("sqlserver")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("sqlite")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("jdbc")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("redis")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("mongodb")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("elasticsearch")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("easysearch")).toBe(true);
|
|
expect(supportsExecutionTargetPicker("qdrant")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("milvus")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("weaviate")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("chromadb")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("etcd")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("zookeeper")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("mq")).toBe(false);
|
|
expect(supportsExecutionTargetPicker("neo4j")).toBe(false);
|
|
expect(supportsExecutionTargetPicker(undefined)).toBe(false);
|
|
});
|
|
});
|