fix(editor): preserve literals during SQL case conversion

This commit is contained in:
guoyongchang 2026-08-05 11:10:15 +08:00 committed by GitHub
parent 8825a98ca2
commit 200d6f55ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 94 additions and 4 deletions

View File

@ -25,6 +25,7 @@ import { blankLineDeletionChanges, replaceSelectedEditorText } from "@/lib/edito
import { createSqlSignatureTooltipDom } from "@/lib/editor/sqlSignatureTooltip";
import { buildSqlInConditionFromPasteSource, insertTextForSqlInCondition } from "@/lib/sql/sqlInListPaste";
import { resolveSqlSingleQuoteKeyAction } from "@/lib/sql/sqlQuoteCaret";
import { convertSqlSelectionCase, type SqlSelectionCaseMode } from "@/lib/sql/sqlSelectionCase";
import { formatMongoShellText } from "@/lib/mongo/mongoFormatter";
import { useConnectionStore, COMPLETION_METADATA_CONCURRENCY } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
@ -420,7 +421,6 @@ function runStatementGutterExtension(markers = props.statementExecutionMarkers ?
return shouldShowStatementGutter(showRunButtons, markers.length) ? (buildRunStatementGutterExtension?.() ?? []) : [];
}
type SelectionCaseMode = "upper" | "lower";
let executableStatementRangeCache: ExecutableStatementRangeCache | null = null;
let editorScrollbarPointerCleanup: (() => void) | null = null;
let editorSelectionDragCleanup: (() => void) | null = null;
@ -1078,17 +1078,17 @@ function selectAllSqlFromContextMenu() {
focusEditor();
}
function convertSelectedSqlCase(mode: SelectionCaseMode): boolean {
function convertSelectedSqlCase(mode: SqlSelectionCaseMode): boolean {
const currentView = view.value;
const EditorSelection = codeMirrorEditorSelection;
if (!currentView || !EditorSelection) return false;
const state = currentView.state;
const documentText = state.doc.toString();
const transaction = state.changeByRange((range) => {
if (range.empty) return { range };
const selectedText = state.sliceDoc(range.from, range.to);
const convertedText = mode === "upper" ? selectedText.toUpperCase() : selectedText.toLowerCase();
const convertedText = convertSqlSelectionCase(documentText, { from: range.from, to: range.to }, mode, sqlBehaviorDialect());
return {
changes: { from: range.from, to: range.to, insert: convertedText },
range: EditorSelection.range(range.from, range.from + convertedText.length),

View File

@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { convertSqlSelectionCase } from "@/lib/sql/sqlSelectionCase";
describe("convertSqlSelectionCase", () => {
it("converts SQL text without changing string literals", () => {
const sql = "SELECT Code FROM Orders WHERE Code = 'ABC001' AND Note = 'It''s Ready'";
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "lower")).toBe("select code from orders where code = 'ABC001' and note = 'It''s Ready'");
});
it("preserves string literals when converting to uppercase", () => {
const sql = "select code from orders where code = 'abc001'";
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "upper")).toBe("SELECT CODE FROM ORDERS WHERE CODE = 'abc001'");
});
it("preserves the selected fragment when the selection is inside a string literal", () => {
const sql = "select * from orders where code = 'AbC001'";
const from = sql.indexOf("bC");
expect(convertSqlSelectionCase(sql, { from, to: from + 2 }, "lower")).toBe("bC");
});
it("preserves PostgreSQL dollar-quoted string literals", () => {
const sql = "select $tag$Mixed Value$tag$ as label";
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "upper", "postgres")).toBe("SELECT $tag$Mixed Value$tag$ AS LABEL");
});
it("continues converting comments and quoted identifiers", () => {
const sql = 'select "MixedName" -- Keep Comment\nfrom users';
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "lower")).toBe('select "mixedname" -- keep comment\nfrom users');
});
it("uses SQL Server tokenization so temp tables do not hide later literals", () => {
const sql = "SELECT * FROM #Temp WHERE Code = 'AbC001'";
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "lower", "sqlserver")).toBe("select * from #temp where code = 'AbC001'");
});
it("preserves MySQL double-quoted strings", () => {
const sql = 'SELECT "Mixed Value" AS Label';
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "lower", "mysql")).toBe('select "Mixed Value" as label');
});
it("preserves MySQL executable comments", () => {
const sql = "SELECT 1 /*!40101 SET @Name = 'Mixed Value' */ FROM Dual";
expect(convertSqlSelectionCase(sql, { from: 0, to: sql.length }, "lower", "mysql")).toBe("select 1 /*!40101 SET @Name = 'Mixed Value' */ from dual");
});
});

View File

@ -0,0 +1,37 @@
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
export type SqlSelectionCaseMode = "upper" | "lower";
type SqlSelectionRange = {
from: number;
to: number;
};
function convertCase(text: string, mode: SqlSelectionCaseMode): string {
return mode === "upper" ? text.toUpperCase() : text.toLowerCase();
}
export function convertSqlSelectionCase(sql: string, range: SqlSelectionRange, mode: SqlSelectionCaseMode, dialectId?: "mysql" | "postgres" | "sqlserver"): string {
const from = Math.max(0, Math.min(range.from, sql.length));
const to = Math.max(from, Math.min(range.to, sql.length));
const protectedTokens = tokenizeSqlSemantic(sql, dialectId).filter((item) => {
if (item.span.end <= from || item.span.start >= to) return false;
if (item.kind === "string") return true;
if (dialectId !== "mysql") return false;
if (item.kind === "quoted_identifier" && item.quote === '"') return true;
return item.kind === "comment" && /^\/\*(?:!|M!)/i.test(item.text);
});
if (protectedTokens.length === 0) return convertCase(sql.slice(from, to), mode);
let converted = "";
let cursor = from;
for (const item of protectedTokens) {
const literalFrom = Math.max(from, item.span.start);
const literalTo = Math.min(to, item.span.end);
converted += convertCase(sql.slice(cursor, literalFrom), mode);
converted += sql.slice(literalFrom, literalTo);
cursor = literalTo;
}
converted += convertCase(sql.slice(cursor, to), mode);
return converted;
}