fix(editor): preserve literals during SQL case conversion
This commit is contained in:
parent
8825a98ca2
commit
200d6f55ab
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue