fix(sqlserver): diagnose DECLARE without data type in routine bodies

This commit is contained in:
zipg
2026-09-21 11:38:04 +08:00
committed by GitHub
parent 2271d840d8
commit 79d6ab64e9
8 changed files with 538 additions and 3 deletions
@@ -136,6 +136,7 @@ import { createHoverSearch, type HoverSearchController } from "@/lib/editor/sqlH
import { lineColumnToOffset, sqlErrorDecorationRange as resolveSqlErrorDecorationRange, sqlErrorSqlMatchesEditor } from "@/lib/sql/sqlDiagnostics";
import { analyzeMysqlRoutineSyntax, supportsMysqlRoutineSyntaxDiagnostics } from "@/lib/sql/mysqlRoutineSyntaxDiagnostics";
import { buildOracleSyntaxDiagnostics } from "@/lib/sql/oracleSyntaxDiagnostics";
import { buildSqlServerRoutineSyntaxDiagnostics } from "@/lib/sql/sqlServerRoutineSyntaxDiagnostics";
import {
DBX_TABLE_REFERENCE_MIME,
DBX_TABLE_REFERENCE_DROP_EVENT,
@@ -217,6 +218,7 @@ import {
isSqlVirtualTableReference,
shouldRunSqlSemanticDiagnostics,
sqlSemanticDiagnosticRangesForViewport,
sqlServerRoutineDefinitionRangesForViewport,
tableReferenceKey,
type SqlSemanticDiagnostic,
} from "@/lib/sql/semantic/diagnostics";
@@ -3940,7 +3942,11 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
executableStatementRangeCache = executableStatementRangeCacheForDoc(executableStatementRangeCache, currentView.state.doc, props.databaseType, sqlStatementParameterOptions());
}
const diagnosticRanges = sqlSemanticDiagnosticRangesForViewport(sql, visibleRanges, props.databaseType, props.databaseType === "sqlserver" ? undefined : executableStatementRangeCache?.ranges, sqlStatementParameterOptions());
if (diagnosticRanges.length === 0) {
// SQL Server routine batches are excluded from `diagnosticRanges` (see
// `sqlServerRoutineDefinitionRangesForViewport`), so they are recomputed here
// and stay part of the replaced range set below.
const sqlServerRoutineRanges = props.databaseType === "sqlserver" ? sqlServerRoutineDefinitionRangesForViewport(sql, visibleRanges) : [];
if (diagnosticRanges.length === 0 && sqlServerRoutineRanges.length === 0) {
if (!options.preserveOutsideRanges) setSemanticDiagnostics([]);
return;
}
@@ -3953,6 +3959,12 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
return !!diagnosticRange && diagnosticRanges.some((range) => rangesOverlap(diagnosticRange, range));
}),
);
// The analyzer never sees routine batches (the MsSql grammar cannot parse their
// parameter list), so run the token-based routine syntax rules instead of leaving
// a stored procedure without any check at all (dbx#9315).
for (const range of sqlServerRoutineRanges) {
nextDiagnostics.push(...offsetSqlSemanticDiagnostics(buildSqlServerRoutineSyntaxDiagnostics(range.sql, props.databaseType), range, sql));
}
const mysqlRoutineAnalysis = props.databaseType === "mysql" && supportsMysqlRoutineSyntaxDiagnostics(sqlDriverProfile.value) ? analyzeMysqlRoutineSyntax(sql) : null;
if (mysqlRoutineAnalysis) {
nextDiagnostics.push(
@@ -4018,7 +4030,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
}
}
if (options.preserveOutsideRanges) {
replaceSemanticDiagnosticsInRanges(nextDiagnostics, diagnosticRanges, sql);
replaceSemanticDiagnosticsInRanges(nextDiagnostics, [...diagnosticRanges, ...sqlServerRoutineRanges], sql);
} else {
setSemanticDiagnostics(nextDiagnostics.sort(compareSqlSemanticDiagnostics));
}
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
/**
* SQL Server 例程批次的语法诊断接线(#9315)写在 `<script setup>` 内部,
* 无法直接 import,因此沿用仓库既有做法(QueryEditor.cteNavigationWiring.spec.ts)
* 对源码做结构断言。规则本身由 sqlServerRoutineSyntaxDiagnostics.test.ts 覆盖,
* 批次/视口裁剪由 sqlSemanticDiagnostics.test.ts 覆盖,这里只锁定接线契约。
*/
const source = readFileSync(new URL("../QueryEditor.vue", import.meta.url), "utf8");
function normalizeCode(text: string): string {
return text.replace(/\s+/g, "").replace(/,(?=[)\]}])/g, "");
}
const normalizedSource = normalizeCode(source);
function expectSourceToContain(fragment: string): void {
expect(normalizedSource).toContain(normalizeCode(fragment));
}
describe("QueryEditor 的 SQL Server 例程语法诊断接线", () => {
it("例程批次被跳过整个语义范围后仍单独取回", () => {
// sqlSemanticDiagnosticRangesForViewport 对例程批次返回空,所以必须再取一次。
expectSourceToContain('const sqlServerRoutineRanges = props.databaseType === "sqlserver" ? sqlServerRoutineDefinitionRangesForViewport(sql, visibleRanges) : [];');
// 例程批次不能因为普通范围为空就提前 return,否则过程体永远不检查。
expectSourceToContain("if (diagnosticRanges.length === 0 && sqlServerRoutineRanges.length === 0) {");
});
it("例程批次只跑 token 规则,不做引用分析", () => {
expectSourceToContain("nextDiagnostics.push(...offsetSqlSemanticDiagnostics(buildSqlServerRoutineSyntaxDiagnostics(range.sql, props.databaseType), range, sql));");
});
it("增量刷新时把例程批次一并纳入替换范围,避免旧诊断残留", () => {
expectSourceToContain("replaceSemanticDiagnosticsInRanges(nextDiagnostics, [...diagnosticRanges, ...sqlServerRoutineRanges], sql);");
});
});
@@ -41,6 +41,28 @@ export function sqlSemanticDiagnosticRangesForViewport(sql: string, visibleRange
return selected;
}
/**
* SQL Server routine definition batches (`CREATE/ALTER PROCEDURE | FUNCTION`) are
* deliberately excluded from reference diagnostics: the MsSql grammar in the
* analyzer cannot parse the parameter list, and metadata checks inside a routine
* body (temp tables, table variables) would report noise (see
* `isSqlServerRoutineDefinitionBatch`). That suppression also dropped the
* *syntax* errors of the body, so a routine that does not even compile looked
* clean (issue #9315). Callers use these ranges to run the routine-only syntax
* rules that survive the suppression, while reference diagnostics stay off.
*/
export function sqlServerRoutineDefinitionRangesForViewport(sql: string, visibleRanges: readonly SqlSemanticDiagnosticVisibleRange[]): SqlTextRange[] {
if (visibleRanges.length === 0) return [];
const selected: SqlTextRange[] = [];
for (const batch of sqlServerBatchRanges(sql)) {
if (!isSqlServerRoutineDefinitionBatch(batch.sql)) continue;
if (!visibleRanges.some((visibleRange) => rangesIntersect(batch, visibleRange))) continue;
selected.push(batch);
}
return selected;
}
function sqlServerSemanticDiagnosticRanges(sql: string): SqlTextRange[] {
const ranges: SqlTextRange[] = [];
for (const batch of sqlServerBatchRanges(sql)) {
@@ -0,0 +1,305 @@
import type { SqlSemanticDiagnostic } from "@/lib/sql/semantic/diagnostics";
import type { DatabaseType, SqlTextSpan } from "@/types/database";
export const SQLSERVER_DECLARE_MISSING_DATA_TYPE_MESSAGE = "T-SQL DECLARE requires a data type before the default value";
type SqlServerTokenKind = "word" | "variable" | "symbol";
interface SqlServerToken {
kind: SqlServerTokenKind;
value: string;
from: number;
to: number;
}
export function supportsSqlServerRoutineSyntaxDiagnostics(databaseType?: DatabaseType): boolean {
return databaseType === "sqlserver";
}
/**
* Syntax rules for T-SQL routine bodies (`CREATE/ALTER PROCEDURE | FUNCTION`).
*
* Routine batches are skipped by the semantic diagnostic pipeline (the analyzer's
* MsSql grammar cannot parse the parameter list), so nothing inside a stored
* procedure was ever checked while editing it. Re-parsing the body with that same
* grammar is not an option either: valid T-SQL such as `WHILE ... BEGIN ... END`,
* `IF/ELSE`, `TRY/CATCH` and `GOTO` make it report phantom errors. The rules here
* are token based, so only constructs the server itself rejects are flagged
* (issue #9315).
*/
export function buildSqlServerRoutineSyntaxDiagnostics(source: string, databaseType?: DatabaseType): SqlSemanticDiagnostic[] {
if (!supportsSqlServerRoutineSyntaxDiagnostics(databaseType)) return [];
const diagnostics: SqlSemanticDiagnostic[] = [];
for (const token of missingDeclareDataTypeTokens(source)) diagnostics.push(diagnosticAtToken(source, token));
return diagnostics;
}
function diagnosticAtToken(source: string, token: SqlServerToken): SqlSemanticDiagnostic {
return {
span: spanForOffsets(source, token.from, token.to),
message: SQLSERVER_DECLARE_MISSING_DATA_TYPE_MESSAGE,
severity: "error",
};
}
/**
* `DECLARE @name = value` is not valid T-SQL: the declaration needs a data type
* (`DECLARE @name INT = value`, an optional `AS` is allowed in between). The
* server rejects the statement with `Incorrect syntax near '='`, so the same
* position is reported here.
*
* Only positions that can actually start a declaration item are inspected — the
* declaration keyword itself, or a top level comma continuing its list — so
* assignment statements such as `SELECT @name = value` or `SET @name = value`
* are never flagged.
*/
function missingDeclareDataTypeTokens(source: string): SqlServerToken[] {
const tokens = tokenizeSqlServerSyntax(source);
const violations: SqlServerToken[] = [];
let index = 0;
let expectsDeclaration = false;
while (index < tokens.length) {
const token = tokens[index];
if (!expectsDeclaration) {
expectsDeclaration = token.kind === "word" && token.value === "DECLARE";
index += 1;
continue;
}
if (token.kind !== "variable") {
// `DECLARE name CURSOR FOR ...` names a cursor without an `@`; anything
// else ends the declaration list we were tracking.
expectsDeclaration = false;
continue;
}
let cursor = index + 1;
if (tokens[cursor]?.kind === "word" && tokens[cursor].value === "AS") cursor += 1;
if (tokens[cursor]?.kind === "symbol" && tokens[cursor].value === "=") {
violations.push(tokens[cursor]);
// The declaration is broken, but the list may continue after the value;
// keep tracking it so every untyped entry is reported.
cursor = skipInitializerValue(tokens, cursor + 1);
} else {
// The item has a data type (or `TABLE`/`CURSOR`); skip the type, and the
// optional `= value` initializer, to find whether the list continues.
cursor = skipDeclarationItem(tokens, cursor);
}
if (tokens[cursor]?.kind === "symbol" && tokens[cursor].value === ",") {
index = cursor + 1;
continue;
}
index = cursor;
expectsDeclaration = false;
}
return violations;
}
/** Advances past a declared data type plus an optional `= value` initializer. */
function skipDeclarationItem(tokens: readonly SqlServerToken[], start: number): number {
let index = start;
if (tokens[index]?.kind === "word") index += 1;
else if (tokens[index]?.kind === "variable") index += 1;
if (tokens[index]?.kind === "symbol" && tokens[index].value === "(") index = skipBalancedParentheses(tokens, index);
if (tokens[index]?.kind === "symbol" && tokens[index].value === "=") {
index = skipInitializerValue(tokens, index + 1);
}
return index;
}
function skipInitializerValue(tokens: readonly SqlServerToken[], start: number): number {
let index = start;
while (index < tokens.length) {
const token = tokens[index];
if (token.kind === "symbol" && token.value === ",") return index;
if (token.kind === "symbol" && token.value === "(") {
index = skipBalancedParentheses(tokens, index);
continue;
}
if (token.kind === "symbol" && token.value === ";") return index;
if (token.kind === "word" && STATEMENT_BOUNDARY_KEYWORDS.has(token.value)) return index;
index += 1;
}
return index;
}
function skipBalancedParentheses(tokens: readonly SqlServerToken[], openingIndex: number): number {
let depth = 0;
let index = openingIndex;
while (index < tokens.length) {
const token = tokens[index];
if (token.kind === "symbol" && token.value === "(") depth += 1;
else if (token.kind === "symbol" && token.value === ")") {
depth -= 1;
if (depth === 0) return index + 1;
}
index += 1;
}
return index;
}
// Statement keywords that end an un-terminated declaration list (`DECLARE @a INT`
// followed by the next statement without a semicolon).
const STATEMENT_BOUNDARY_KEYWORDS = new Set(["SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "SET", "EXEC", "EXECUTE", "IF", "WHILE", "RETURN", "PRINT", "RAISERROR", "THROW", "WITH", "BEGIN", "GO"]);
function tokenizeSqlServerSyntax(source: string): SqlServerToken[] {
const tokens: SqlServerToken[] = [];
let index = 0;
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
if (char === "-" && next === "-") {
index = skipLineComment(source, index);
continue;
}
if (char === "/" && next === "*") {
index = skipBlockComment(source, index);
continue;
}
if (char === "'" || char === '"') {
index = skipQuotedLiteral(source, index, char);
continue;
}
if (char === "[") {
index = skipBracketedIdentifier(source, index);
continue;
}
if (char === "@") {
const end = readWordEnd(source, index + 1);
if (end > index + 1) {
tokens.push({ kind: "variable", value: source.slice(index + 1, end).toUpperCase(), from: index, to: end });
index = end;
continue;
}
tokens.push({ kind: "symbol", value: char, from: index, to: index + 1 });
index += 1;
continue;
}
if (/[A-Za-z_]/.test(char)) {
const end = readWordEnd(source, index);
tokens.push({ kind: "word", value: source.slice(index, end).toUpperCase(), from: index, to: end });
index = end;
continue;
}
if (/[0-9]/.test(char)) {
const end = readNumberEnd(source, index);
index = end;
continue;
}
if (char === "#") {
const end = readWordEnd(source, index + 1);
tokens.push({ kind: "word", value: source.slice(index, end).toUpperCase(), from: index, to: end });
index = end;
continue;
}
if (/[ \t\r\n]/.test(char)) {
index += 1;
continue;
}
tokens.push({ kind: "symbol", value: char, from: index, to: index + 1 });
index += 1;
}
return tokens;
}
function readWordEnd(source: string, start: number): number {
let index = start;
while (index < source.length && /[A-Za-z0-9_$#@]/.test(source[index])) index += 1;
return index;
}
function readNumberEnd(source: string, start: number): number {
let index = start;
while (index < source.length && /[0-9A-Za-z_.]/.test(source[index])) index += 1;
return index;
}
function skipLineComment(source: string, start: number): number {
const newline = source.indexOf("\n", start);
return newline < 0 ? source.length : newline + 1;
}
// T-SQL nests block comments, so the closing marker has to be matched by depth.
function skipBlockComment(source: string, start: number): number {
let depth = 1;
let index = start + 2;
while (index < source.length) {
if (source.startsWith("/*", index)) {
depth += 1;
index += 2;
continue;
}
if (source.startsWith("*/", index)) {
depth -= 1;
index += 2;
if (depth === 0) return index;
continue;
}
index += 1;
}
return source.length;
}
function skipQuotedLiteral(source: string, start: number, quote: string): number {
let index = start + 1;
while (index < source.length) {
if (source[index] !== quote) {
index += 1;
continue;
}
if (source[index + 1] === quote) {
index += 2;
continue;
}
return index + 1;
}
return source.length;
}
function skipBracketedIdentifier(source: string, start: number): number {
let index = start + 1;
while (index < source.length) {
if (source[index] !== "]") {
index += 1;
continue;
}
if (source[index + 1] === "]") {
index += 2;
continue;
}
return index + 1;
}
return source.length;
}
function spanForOffsets(source: string, startOffset: number, endOffset: number): SqlTextSpan {
const start = lineColumnAtOffset(source, startOffset);
const end = lineColumnAtOffset(source, endOffset);
return {
start_line: start.line,
start_column: start.column,
end_line: end.line,
end_column: Math.max(end.column - 1, start.column),
};
}
function lineColumnAtOffset(source: string, offset: number): { line: number; column: number } {
const clamped = Math.max(0, Math.min(offset, source.length));
let line = 1;
let column = 1;
for (let index = 0; index < clamped; index += 1) {
if (source[index] === "\n") {
line += 1;
column = 1;
continue;
}
column += 1;
}
return { line, column };
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

+71 -1
View File
@@ -1,6 +1,6 @@
import { strict as assert } from "node:assert";
import { test } from "vitest";
import { buildSqlParserErrorDiagnostic, buildSqlSemanticDiagnostics, areSqlSemanticDiagnosticsEqual, isSqlSemanticDiagnosticInputContext, shouldRunSqlSemanticDiagnostics, sqlSemanticDiagnosticRangesForViewport } from "../../apps/desktop/src/lib/sql/semantic/diagnostics.ts";
import { buildSqlParserErrorDiagnostic, buildSqlSemanticDiagnostics, areSqlSemanticDiagnosticsEqual, isSqlSemanticDiagnosticInputContext, shouldRunSqlSemanticDiagnostics, sqlSemanticDiagnosticRangesForViewport, sqlServerRoutineDefinitionRangesForViewport } from "../../apps/desktop/src/lib/sql/semantic/diagnostics.ts";
import { sqlReferenceAnalysisDialectFor } from "../../apps/desktop/src/lib/sql/semantic/dialect.ts";
import type { SqlReferenceAnalysis } from "../../apps/desktop/src/types/database.ts";
@@ -620,3 +620,73 @@ test("uses executable soft statement ranges for viewport diagnostics", () => {
["SELECT missing_field FROM second"],
);
});
const PROCEDURE_SQL = `ALTER PROCEDURE [COMMON].[TEST]
@BeginTime varchar(100)=null
AS
BEGIN
SET NOCOUNT ON;
DECLARE @temp_nums = 2;
END`;
test("recovers SQL Server routine batches for routine-only syntax rules", () => {
const ranges = sqlServerRoutineDefinitionRangesForViewport(PROCEDURE_SQL, [{ from: 0, to: PROCEDURE_SQL.length }]);
assert.deepEqual(
ranges.map((range) => ({ from: range.from, to: range.to })),
[{ from: 0, to: PROCEDURE_SQL.length }],
);
assert.equal(ranges[0]?.sql, PROCEDURE_SQL);
assert.deepEqual(sqlSemanticDiagnosticRangesForViewport(PROCEDURE_SQL, [{ from: 0, to: PROCEDURE_SQL.length }], "sqlserver"), []);
});
test("keeps SQL Server routine batches separated by GO, but skips leading query batches", () => {
const sql = `SELECT 1;
GO
CREATE OR ALTER PROCEDURE dbo.refresh_users AS
BEGIN
DECLARE @temp_nums = 2;
END
GO
ALTER FUNCTION [COMMON].[uf_answer]()
RETURNS int
AS
BEGIN
RETURN 42;
END`;
const ranges = sqlServerRoutineDefinitionRangesForViewport(sql, [{ from: 0, to: sql.length }]);
assert.deepEqual(
ranges.map((range) => range.sql),
[
"CREATE OR ALTER PROCEDURE dbo.refresh_users AS\nBEGIN\n DECLARE @temp_nums = 2;\nEND\n",
"ALTER FUNCTION [COMMON].[uf_answer]()\nRETURNS int\nAS\nBEGIN\n RETURN 42;\nEND",
],
);
});
test("only reports SQL Server routine batches that intersect the viewport", () => {
const sql = `CREATE PROCEDURE dbo.p_test AS
BEGIN
DECLARE @temp_nums = 2;
END
GO
SELECT missing_field FROM dbo.users;`;
const visibleFrom = sql.indexOf("SELECT missing_field");
const ranges = sqlServerRoutineDefinitionRangesForViewport(sql, [{ from: visibleFrom, to: sql.length }]);
assert.deepEqual(ranges, []);
const procedureFrom = sql.indexOf("CREATE PROCEDURE");
const visibleProcedure = sqlServerRoutineDefinitionRangesForViewport(sql, [{ from: procedureFrom, to: procedureFrom + 10 }]);
assert.equal(visibleProcedure.length, 1);
assert.equal(visibleProcedure[0]?.from, procedureFrom);
});
test("ignores SQL Server non-routine batches and empty viewports", () => {
const sql = "SELECT missing_field FROM dbo.users;\nGO\nUPDATE dbo.users SET name = 'x';";
assert.deepEqual(sqlServerRoutineDefinitionRangesForViewport(sql, [{ from: 0, to: sql.length }]), []);
assert.deepEqual(sqlServerRoutineDefinitionRangesForViewport(PROCEDURE_SQL, []), []);
});
@@ -0,0 +1,89 @@
import { expect, test } from "vitest";
import { buildSqlServerRoutineSyntaxDiagnostics, supportsSqlServerRoutineSyntaxDiagnostics, SQLSERVER_DECLARE_MISSING_DATA_TYPE_MESSAGE } from "../../apps/desktop/src/lib/sql/sqlServerRoutineSyntaxDiagnostics.ts";
function messages(sql: string, databaseType: Parameters<typeof buildSqlServerRoutineSyntaxDiagnostics>[1] = "sqlserver") {
return buildSqlServerRoutineSyntaxDiagnostics(sql, databaseType).map((diagnostic) => diagnostic.message);
}
function spanAt(sql: string, token: string, occurrence = 0) {
const upperSql = sql.toUpperCase();
const upperToken = token.toUpperCase();
let offset = -1;
let from = 0;
for (let index = 0; index <= occurrence; index += 1) {
offset = upperSql.indexOf(upperToken, from);
from = offset + token.length;
}
expect(offset).not.toBe(-1);
const lineStart = sql.lastIndexOf("\n", offset - 1) + 1;
const line = sql.slice(0, offset).split(/\r?\n/).length;
return { start_line: line, start_column: offset - lineStart + 1, end_line: line, end_column: offset - lineStart + token.length };
}
test("reports a DECLARE default value that has no data type", () => {
const sql = "ALTER PROCEDURE dbo.p_test @x INT AS\nBEGIN\n DECLARE @temp_nums = 2;\n SELECT @temp_nums AS v;\nEND";
const diagnostics = buildSqlServerRoutineSyntaxDiagnostics(sql, "sqlserver");
expect(diagnostics.map((diagnostic) => diagnostic.message)).toEqual([SQLSERVER_DECLARE_MISSING_DATA_TYPE_MESSAGE]);
expect(diagnostics[0]?.span).toEqual(spanAt(sql, "=", 0));
expect(diagnostics[0]?.severity).toBe("error");
});
test("reports every untyped declaration in a comma separated list", () => {
const sql = "DECLARE @a INT = 1, @b = 2, @c VARCHAR(5) = 'x', @d = 4;";
const diagnostics = buildSqlServerRoutineSyntaxDiagnostics(sql, "sqlserver");
expect(diagnostics.map((diagnostic) => diagnostic.span)).toEqual([spanAt(sql, "=", 1), spanAt(sql, "=", 3)]);
});
test("tracks CRLF and mixed-case declarations", () => {
const sql = "declare\r\n @first = 1,\r\n @second as = 2;";
const diagnostics = buildSqlServerRoutineSyntaxDiagnostics(sql, "sqlserver");
expect(diagnostics.map((diagnostic) => diagnostic.span)).toEqual([spanAt(sql, "=", 0), spanAt(sql, "=", 1)]);
});
test.each([
"DECLARE @a INT = 1;",
"DECLARE @a AS INT = 1;",
"DECLARE @a DECIMAL(10,2) = 1.5, @b VARCHAR(20) = 'x,y';",
"DECLARE @a INT, @b VARCHAR(5) = 'z';",
"DECLARE @a INT;",
"DECLARE @a dbo.MyType = 1;",
"DECLARE @t TABLE (id INT, name VARCHAR(10) DEFAULT 'a,b');",
"DECLARE @t TABLE (id INT, CONSTRAINT pk PRIMARY KEY (id));",
"DECLARE @c CURSOR;",
"DECLARE cur CURSOR FOR SELECT id, name FROM dbo.t;",
"DECLARE @c CURSOR FOR SELECT a, b FROM dbo.t;",
"SELECT @a = 1; SET @b = 2; UPDATE dbo.t SET @c = 3 WHERE id = 1;",
"EXEC dbo.p @a = 1, @b = 2;",
"SELECT * FROM dbo.t WHERE a = 1, b = 2;",
"-- DECLARE @a = 1\n/* DECLARE @b = 2 */",
"PRINT 'DECLARE @a = 1';",
"EXEC sp_executesql N'DECLARE @a = 1';",
"CREATE PROCEDURE dbo.p @a INT = 1 AS BEGIN SELECT @a; END",
"CREATE FUNCTION dbo.f(@a INT = 1) RETURNS INT AS BEGIN RETURN @a; END",
"DECLARE @a INT = (SELECT TOP 1 id FROM dbo.t);",
])("accepts the valid T-SQL declaration %s", (sql) => {
expect(messages(sql, "sqlserver")).toEqual([]);
});
test("stops at the statement that follows an unterminated declaration list", () => {
const sql = "DECLARE @a INT\nSELECT @b = 1\nDECLARE @c = 2";
const diagnostics = buildSqlServerRoutineSyntaxDiagnostics(sql, "sqlserver");
expect(diagnostics.map((diagnostic) => diagnostic.span)).toEqual([spanAt(sql, "=", 1)]);
});
test("stays silent for other dialects", () => {
const sql = "DECLARE @a = 1;";
expect(messages(sql, "mysql")).toEqual([]);
expect(messages(sql, "oracle")).toEqual([]);
expect(buildSqlServerRoutineSyntaxDiagnostics(sql, undefined)).toEqual([]);
});
test("only supports SQL Server", () => {
expect(supportsSqlServerRoutineSyntaxDiagnostics("sqlserver")).toBe(true);
expect(supportsSqlServerRoutineSyntaxDiagnostics("mysql")).toBe(false);
expect(supportsSqlServerRoutineSyntaxDiagnostics(undefined)).toBe(false);
});