fix(editor): show insert value hints for all statements in a window
This commit is contained in:
4 files changed
+116
-3
No files matched your search
@@ -686,14 +686,52 @@ export function parseInsertValuesClausesInRanges(sql: string, ranges: readonly T
|
||||
return clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split one statement group into its individual INSERT statements.
|
||||
*
|
||||
* Semicolons are optional in several dialects (T-SQL in particular), so a single top-level group
|
||||
* can hold more than one INSERT statement -- `BEGIN TRANSACTION ... INSERT ... INSERT ... COMMIT`
|
||||
* typed in one query window is the everyday case. `parseInsertClause` only ever reads the first
|
||||
* INSERT of the token list it is handed, so passing it the whole group left every later INSERT
|
||||
* without its VALUES/SELECT-to-column mapping (#9966). Splitting at each INSERT keyword keeps the
|
||||
* per-statement parse working for semicolon-less scripts; scripts that do terminate their
|
||||
* statements with `;` still yield one group (and therefore one segment) per statement.
|
||||
*
|
||||
* An `insert` word is only a statement start when the next token is `into`: MySQL/MariaDB also
|
||||
* expose the INSERT(str, pos, len, newstr) string function, and splitting at that call would
|
||||
* truncate its host statement mid-expression (parseInsertClause requires INTO anyway).
|
||||
*/
|
||||
function splitInsertStatements(tokens: readonly SqlSemanticToken[]): Array<{ span: SqlSemanticSpan; tokens: SqlSemanticToken[] }> {
|
||||
const starts: number[] = [];
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const item = tokens[index];
|
||||
const next = tokens[index + 1];
|
||||
if (item?.kind === "word" && item.normalized === "insert" && next?.kind === "word" && next.normalized === "into") {
|
||||
starts.push(index);
|
||||
}
|
||||
}
|
||||
const statements: Array<{ span: SqlSemanticSpan; tokens: SqlSemanticToken[] }> = [];
|
||||
for (let position = 0; position < starts.length; position += 1) {
|
||||
const from = starts[position]!;
|
||||
const to = position + 1 < starts.length ? starts[position + 1]! : tokens.length;
|
||||
const statementTokens = tokens.slice(from, to);
|
||||
const first = statementTokens[0]!;
|
||||
const last = statementTokens[statementTokens.length - 1]!;
|
||||
statements.push({ span: { start: first.span.start, end: last.span.end }, tokens: statementTokens });
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
|
||||
/** Parse all INSERT ... VALUES/SELECT clauses in `sql` (multi-statement aware). Prefer ranged parsing for editors. */
|
||||
export function parseInsertValuesClauses(sql: string, dialectId = "mysql"): InsertValuesClause[] {
|
||||
if (!sql.trim()) return [];
|
||||
const allTokens = tokenizeSqlSemantic(sql, dialectId);
|
||||
const clauses: InsertValuesClause[] = [];
|
||||
for (const { span, tokens } of statementTokenGroups(sql, allTokens)) {
|
||||
const clause = parseInsertClause(tokens, span);
|
||||
if (clause) clauses.push(clause);
|
||||
for (const { tokens } of statementTokenGroups(sql, allTokens)) {
|
||||
for (const statement of splitInsertStatements(tokens)) {
|
||||
const clause = parseInsertClause(statement.tokens, statement.span);
|
||||
if (clause) clauses.push(clause);
|
||||
}
|
||||
}
|
||||
return clauses;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
+75
@@ -583,6 +583,81 @@ test("documents a known limitation of the pure-string fallback (no live EditorSt
|
||||
assert.notEqual(window.from, 0, "known-imperfect (pure-string fallback only): a correct implementation would return 0 here (the whole CREATE FUNCTION is one statement)");
|
||||
});
|
||||
|
||||
test("parses every INSERT inside a semicolon-less statement group", () => {
|
||||
// T-SQL scripts routinely omit statement terminators; the reported case is one query window
|
||||
// holding BEGIN TRANSACTION + two INSERT ... SELECT statements (#9966).
|
||||
const sql = ["BEGIN TRANSACTION", "INSERT INTO u_msfx_a (id, billno) SELECT src_id, src_billno FROM staging_a", "INSERT INTO u_msfx_b (id, billno) SELECT src_id, src_billno FROM staging_b", "COMMIT TRANSACTION"].join("\n");
|
||||
const clauses = parseInsertValuesClauses(sql, "sqlserver");
|
||||
assert.deepEqual(
|
||||
clauses.map((clause) => clause.table),
|
||||
["u_msfx_a", "u_msfx_b"],
|
||||
);
|
||||
const hints = buildInsertValueHints(clauses);
|
||||
assert.deepEqual(
|
||||
hints.map((hint) => hint.column),
|
||||
["id", "billno", "id", "billno"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
hints.map((hint) => hint.from),
|
||||
[sql.indexOf("src_id"), sql.indexOf("src_billno"), sql.lastIndexOf("src_id"), sql.lastIndexOf("src_billno")],
|
||||
);
|
||||
});
|
||||
|
||||
test("parses every INSERT ... VALUES inside a semicolon-less statement group", () => {
|
||||
const sql = "INSERT INTO t1 (id, name) VALUES (1, 'a')\nINSERT INTO t2 (id, name) VALUES (2, 'b')";
|
||||
const clauses = parseInsertValuesClauses(sql, "sqlserver");
|
||||
assert.deepEqual(
|
||||
clauses.map((clause) => clause.table),
|
||||
["t1", "t2"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildInsertValueHints(clauses).map((hint) => hint.column),
|
||||
["id", "name", "id", "name"],
|
||||
);
|
||||
});
|
||||
|
||||
test("does not split a statement at the INSERT() string function", () => {
|
||||
// MySQL/MariaDB expose INSERT(str, pos, len, newstr); an `insert` word without a following
|
||||
// INTO is a function call, not a statement start.
|
||||
const sql = "INSERT INTO t1 (id, name) SELECT id, INSERT('ab', 1, 2, 'xy') FROM src\nINSERT INTO t2 (id, name) VALUES (1, INSERT('ab', 1, 2, 'xy'))";
|
||||
const clauses = parseInsertValuesClauses(sql, "mysql");
|
||||
assert.deepEqual(
|
||||
clauses.map((clause) => clause.table),
|
||||
["t1", "t2"],
|
||||
);
|
||||
});
|
||||
|
||||
test("parses each batch of a GO-separated SQL Server script", () => {
|
||||
const sql = ["INSERT INTO t1 (id, name) SELECT src_id, src_name FROM staging1", "GO", "INSERT INTO t2 (id, name) SELECT src_id, src_name FROM staging2", "GO"].join("\n");
|
||||
const clauses = parseInsertValuesClauses(sql, "sqlserver");
|
||||
assert.deepEqual(
|
||||
clauses.map((clause) => clause.table),
|
||||
["t1", "t2"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildInsertValueHints(clauses).map((hint) => hint.column),
|
||||
["id", "name", "id", "name"],
|
||||
);
|
||||
});
|
||||
|
||||
test("still parses INSERT statements separated by semicolons", () => {
|
||||
const sql = "INSERT INTO t1 (id, name) SELECT src_id, src_name FROM staging1; INSERT INTO t2 (id, name) SELECT src_id, src_name FROM staging2;";
|
||||
const clauses = parseInsertValuesClauses(sql, "sqlserver");
|
||||
assert.deepEqual(
|
||||
clauses.map((clause) => clause.table),
|
||||
["t1", "t2"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildInsertValueHints(clauses).map((hint) => hint.column),
|
||||
["id", "name", "id", "name"],
|
||||
);
|
||||
});
|
||||
|
||||
test("does not read a MERGE INSERT branch as an INSERT statement", () => {
|
||||
const sql = "MERGE INTO t USING s ON t.id = s.id WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name)";
|
||||
assert.deepEqual(parseInsertValuesClauses(sql, "sqlserver"), []);
|
||||
});
|
||||
|
||||
test("ignores statements that are not INSERT VALUES", () => {
|
||||
const sql = "SELECT 1; UPDATE users SET name = 'a' WHERE id = 1;";
|
||||
assert.deepEqual(parseInsertValueHints(sql), []);
|
||||
|
||||
Reference in new issue
Block a user