diff --git a/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts b/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts index 5e567dbbcb..2b34a666c4 100644 --- a/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/codemirrorSqlDialect.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import * as langSql from "@codemirror/lang-sql"; import { createDbxCodeMirrorSqlDialect, postgresKeywordSyntaxTerms } from "@/lib/editor/codemirrorSqlDialect"; +import type { DatabaseType } from "@/types/database"; describe("codemirrorSqlDialect", () => { it("keeps common PostgreSQL identifier names out of keyword highlighting", () => { @@ -23,4 +24,12 @@ describe("codemirrorSqlDialect", () => { expect(doltBuiltins.has("dolt_merge")).toBe(true); expect(mysqlBuiltins.has("dolt_branch")).toBe(false); }); + + it("keeps double quotes as identifier delimiters for Oracle-family dialects", () => { + const databaseTypes: DatabaseType[] = ["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]; + + for (const databaseType of databaseTypes) { + expect(createDbxCodeMirrorSqlDialect(langSql, "mysql", databaseType).spec.doubleQuotedStrings, databaseType).toBe(false); + } + }); }); diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.syntaxTree.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.syntaxTree.spec.ts index 7938c43141..d86bfdf8b3 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.syntaxTree.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.syntaxTree.spec.ts @@ -3,15 +3,16 @@ import { ensureSyntaxTree } from "@codemirror/language"; import { EditorState } from "@codemirror/state"; import { describe, expect, it } from "vitest"; import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect"; -import { isSqlCompletionSuppressedContext, isSqlLikeCompletionStatement } from "@/lib/sql/sqlCompletion"; +import { isSqlCompletionSuppressedContext, isSqlLikeCompletionStatement, shouldAutoOpenSqlCompletion } from "@/lib/sql/sqlCompletion"; import { buildSqlSemanticModel } from "@/lib/sql/semantic/model"; +import type { DatabaseType } from "@/types/database"; // Forces CM6's background parse to completion so tests are deterministic. Production code never // does this -- see sqlSyntaxTreeWindow.ts's doc comment. -function stateFor(doc: string): EditorState { +function stateFor(doc: string, dialectName: "mysql" | "postgres" | "sqlserver" = "postgres", databaseType: DatabaseType = "postgres"): EditorState { const state = EditorState.create({ doc, - extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, "postgres", "postgres") })], + extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, dialectName, databaseType) })], }); ensureSyntaxTree(state, doc.length, 5_000); return state; @@ -46,6 +47,21 @@ describe("isSqlCompletionSuppressedContext with a live EditorState", () => { expect(isSqlCompletionSuppressedContext(sql, cursor, { databaseType: "postgres", editorState: state })).toBe(false); }); + + it("auto-opens table completion while editing double-quoted Oracle-family names", () => { + const databaseTypes: DatabaseType[] = ["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]; + + for (const databaseType of databaseTypes) { + for (const sql of ['SELECT * FROM "fo"', 'SELECT * FROM "fo']) { + const cursor = sql.endsWith('"') ? sql.length - 1 : sql.length; + const state = stateFor(sql, "mysql", databaseType); + const options = { databaseType, dialect: "mysql" as const, editorState: state }; + + expect(isSqlCompletionSuppressedContext(sql, cursor, options), `${databaseType}: ${sql}`).toBe(false); + expect(shouldAutoOpenSqlCompletion(sql, cursor, options), `${databaseType}: ${sql}`).toBe(true); + } + } + }); }); describe("isSqlLikeCompletionStatement with a live EditorState", () => { diff --git a/apps/desktop/src/lib/__tests__/sql/sqlSyntaxTreeWindow.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlSyntaxTreeWindow.spec.ts index 6895264b71..0cb6b9ed0b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlSyntaxTreeWindow.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlSyntaxTreeWindow.spec.ts @@ -4,14 +4,15 @@ import { EditorState } from "@codemirror/state"; import { describe, expect, it } from "vitest"; import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect"; import { resolveLexicalLeafFromSyntaxTree, resolveStatementWindowFromSyntaxTree } from "@/lib/sql/sqlSyntaxTreeWindow"; +import type { DatabaseType } from "@/types/database"; // Forces CM6's background parse to completion so tests are deterministic instead of racing the // idle-time parse scheduler. Production code never does this (see sqlSyntaxTreeWindow.ts's doc // comment on why it only ever consults syntaxTree/syntaxTreeAvailable, never ensureSyntaxTree). -function stateFor(doc: string, dialectName: "mysql" | "postgres" | "sqlserver" = "postgres", databaseType?: string): EditorState { +function stateFor(doc: string, dialectName: "mysql" | "postgres" | "sqlserver" = "postgres", databaseType?: DatabaseType): EditorState { const state = EditorState.create({ doc, - extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, dialectName, databaseType as never) })], + extensions: [langSql.sql({ dialect: createDbxCodeMirrorSqlDialect(langSql, dialectName, databaseType) })], }); ensureSyntaxTree(state, doc.length, 5_000); return state; @@ -129,6 +130,22 @@ describe("resolveLexicalLeafFromSyntaxTree", () => { expect(resolveLexicalLeafFromSyntaxTree(backtickState, backtickQuoted.indexOf("user_name"))?.inStringLiteral).toBe(false); }); + it("does not treat edited Oracle-family table identifiers as string literals", () => { + const databaseTypes: DatabaseType[] = ["oracle", "dameng", "yashandb", "oscar", "oceanbase-oracle"]; + + for (const databaseType of databaseTypes) { + const paired = 'SELECT * FROM "fo"'; + const pairedCursor = paired.length - 1; + expect(resolveLexicalLeafFromSyntaxTree(stateFor(paired, "mysql", databaseType), pairedCursor)?.inStringLiteral, `${databaseType} paired`).toBe(false); + + const unclosed = 'SELECT * FROM "fo'; + expect(resolveLexicalLeafFromSyntaxTree(stateFor(unclosed, "mysql", databaseType), unclosed.length)?.inStringLiteral, `${databaseType} unclosed`).toBe(false); + + const stringLiteral = "SELECT * FROM users WHERE name = 'fo'"; + expect(resolveLexicalLeafFromSyntaxTree(stateFor(stringLiteral, "mysql", databaseType), stringLiteral.indexOf("fo"))?.inStringLiteral, `${databaseType} string`).toBe(true); + } + }); + it("classifies plain code as neither a comment nor a string literal", () => { const sql = "select id from users"; const cursor = sql.indexOf("id"); diff --git a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts index 0f3b3d106f..4706ea841a 100644 --- a/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts +++ b/apps/desktop/src/lib/editor/codemirrorSqlDialect.ts @@ -180,6 +180,7 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu const baseDialect = codeMirrorBaseDialect(langSql, dialectName, databaseType); const isPostgres = baseDialect === langSql.PostgreSQL; const isSqlServer = baseDialect === langSql.MSSQL; + const isPlsql = baseDialect === langSql.PLSQL; const isClickHouse = databaseType === "clickhouse" || dialectName === "clickhouse"; const baseKeywords = isClickHouse ? standardSqlKeywordSyntaxTerms(langSql) : isPostgres ? postgresKeywordSyntaxTerms(baseDialect.spec.keywords || "") : baseDialect.spec.keywords || ""; const baseTypes = isClickHouse ? STANDARD_SQL_TYPES : baseDialect.spec.types || ""; @@ -197,6 +198,7 @@ export function createDbxCodeMirrorSqlDialect(langSql: CodeMirrorSqlLanguageModu spaceAfterDashes: false, } : {}), + ...(isPlsql ? { doubleQuotedStrings: false } : {}), doubleDollarQuotedStrings: false, }); } diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 552eed41cd..4f2e0efaac 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -948,6 +948,38 @@ test("replaces partially typed quoted identifiers without duplicating quotes", ( } }); +test("suggests same-prefix tables while editing double-quoted Oracle-family identifiers", () => { + for (const databaseType of ["oracle", "dameng"] as const) { + const markedSql = 'SELECT * FROM "Fo|"'; + const cursor = markedSql.indexOf("|"); + const sql = markedSql.replace("|", ""); + const options = { databaseType, dialect: "mysql" as const }; + const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor, options), getSqlCompletionContext(sql, cursor, options)); + const items = buildSqlCompletionItemsFromContext(context, { + tables: [ + { name: "FormAlpha", schema: "APP", type: "table" }, + { name: "FormArchive", schema: "APP", type: "table" }, + ], + columnsByTable: new Map(), + ...options, + }); + const replacement = prepareSqlCompletionReplacement(sql, cursor, context, items); + const tableItems = replacement.items.filter((item) => item.type === "table"); + + assert.deepEqual( + tableItems.map((item) => item.label), + ["FormAlpha", "FormArchive"], + databaseType, + ); + assert.equal(replacement.from, sql.indexOf('"'), databaseType); + assert.equal( + tableItems.every((item) => item.replaceClosingQuote === '"'), + true, + databaseType, + ); + } +}); + test("uses PostgreSQL-style double quotes for PostgreSQL-family completion identifiers", () => { const databaseTypes: DatabaseType[] = ["kingbase", "vastbase", "highgo", "uxdb"];