diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index c263212a73..1a0b7d6159 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -73,7 +73,7 @@ import { startsQueryEditorRectangularSelection } from "@/lib/editor/queryEditorP import { LARGE_PASTE_HISTORY_USER_EVENT, normalizeQueryEditorPasteText, recoverableNativePasteSuffix, shouldRecoverLargeTauriPaste } from "@/lib/editor/queryEditorLargePaste"; import type { StatementExecutionMarker } from "@/lib/tabs/tabPresentation"; import { isSchemaAware, isSingleDatabase, supportsDatabaseSchemaQualifier, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport"; -import { metadataSchemaForConnection } from "@/lib/database/jdbcDialect"; +import { metadataSchemaForConnection, sqlSnippetDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy"; import { queryContextObjectActions, queryContextObjectRoute, queryTableCandidateAtSqlPosition, resolveQueryContextCandidateDatabase, resolveQueryContextObjectTarget, type QueryContextObjectAction } from "@/lib/sql/queryCursorTableTarget"; import * as api from "@/lib/backend/api"; @@ -158,6 +158,10 @@ const settingsStore = useSettingsStore(); const { isDark, themePalette } = useTheme(); const { t } = useI18n(); const { toast } = useToast(); +const snippetDatabaseType = computed(() => { + const connection = props.connectionId ? connectionStore.getConfig(props.connectionId) : undefined; + return sqlSnippetDatabaseTypeForConnection(connection) ?? props.databaseType; +}); const SQL_FUNCTION_NAMES = [ "COUNT", @@ -2493,7 +2497,7 @@ async function provideSqlCompletions(context: CompletionContext) { translations: completionTranslations.value, snippets: settingsStore.editorSettings.snippets, dialect: props.dialect, - databaseType: props.databaseType, + databaseType: snippetDatabaseType.value, currentSchema: props.schema, keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, @@ -2513,7 +2517,7 @@ async function provideSqlCompletions(context: CompletionContext) { translations: completionTranslations.value, snippets: settingsStore.editorSettings.snippets, dialect: props.dialect, - databaseType: props.databaseType, + databaseType: snippetDatabaseType.value, currentSchema: props.schema, keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase, autoAliasTables: settingsStore.editorSettings.autoAliasTables, @@ -2707,7 +2711,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType { + it("keeps the global and Manticore-only built-in snippet inventories explicit", () => { + expect(DEFAULT_SQL_SNIPPETS.map(({ id, prefix }) => [id, prefix])).toEqual([ + ["builtin-sel", "sel"], + ["builtin-ins", "ins"], + ["builtin-upd", "upd"], + ["builtin-cte", "cte"], + ["builtin-join", "join"], + ["builtin-case", "case"], + ["builtin-ct", "ct"], + ["builtin-ex", "ex"], + ["builtin-nex", "nex"], + ["builtin-at", "at"], + ["builtin-ci", "ci"], + ]); + expect(MANTICORESEARCH_SQL_SNIPPETS.map(({ id, prefix }) => [id, prefix])).toEqual([ + ["builtin-manticore-match", "match"], + ["builtin-manticore-facet", "facet"], + ["builtin-manticore-show-meta", "m"], + ["builtin-manticore-show-tables", "tab"], + ["builtin-manticore-call-pq", "p"], + ]); + }); + it("returns matching snippet by prefix", () => { const items = buildSnippetItemsForTest("sel", TEST_SNIPPETS); expect(items).toHaveLength(1); @@ -32,6 +57,49 @@ describe("buildSnippetItems", () => { expect(items[0].apply).toBe("SELECT *\nFROM ${table}\nLIMIT 100;"); }); + it.each([ + ["mysql", "SELECT *\nFROM table\nLIMIT 100;", "SELECT *\nFROM ${table}\nLIMIT 100;"], + ["oracle", "SELECT *\nFROM table\nWHERE ROWNUM <= 100;", "SELECT *\nFROM ${table}\nWHERE ROWNUM <= 100;"], + ["oceanbase-oracle", "SELECT *\nFROM table\nWHERE ROWNUM <= 100;", "SELECT *\nFROM ${table}\nWHERE ROWNUM <= 100;"], + ["oscar", "SELECT *\nFROM table\nWHERE ROWNUM <= 100;", "SELECT *\nFROM ${table}\nWHERE ROWNUM <= 100;"], + ["xugu", "SELECT *\nFROM table\nLIMIT 100;", "SELECT *\nFROM ${table}\nLIMIT 100;"], + ["yashandb", "SELECT *\nFROM table\nLIMIT 100;", "SELECT *\nFROM ${table}\nLIMIT 100;"], + ["dameng", "SELECT *\nFROM table\nFETCH FIRST 100 ROWS ONLY;", "SELECT *\nFROM ${table}\nFETCH FIRST 100 ROWS ONLY;"], + ["db2", "SELECT *\nFROM table\nFETCH FIRST 100 ROWS ONLY;", "SELECT *\nFROM ${table}\nFETCH FIRST 100 ROWS ONLY;"], + ["sqlserver", "SELECT TOP 100 *\nFROM table;", "SELECT TOP 100 *\nFROM ${table};"], + ["access", "SELECT TOP 100 *\nFROM table;", "SELECT TOP 100 *\nFROM ${table};"], + ["iris", "SELECT TOP 100 *\nFROM table;", "SELECT TOP 100 *\nFROM ${table};"], + ["teradata", "SELECT TOP 100 *\nFROM table;", "SELECT TOP 100 *\nFROM ${table};"], + ["informix", "SELECT FIRST 100 *\nFROM table;", "SELECT FIRST 100 *\nFROM ${table};"], + ["firebird", "SELECT *\nFROM table\nROWS 100;", "SELECT *\nFROM ${table}\nROWS 100;"], + ["jdbc", "SELECT *\nFROM table;", "SELECT *\nFROM ${table};"], + ] as const)("uses the %s row limit syntax in the built-in select snippet", (databaseType, detail, apply) => { + const items = buildSnippetItemsForTest("sel", [BUILTIN_SELECT], undefined, databaseType); + + expect(items[0].detail).toBe(detail); + expect(items[0].apply).toBe(apply); + }); + + it("passes the active database type through the SQL completion provider", () => { + const items = buildSqlCompletionItems("sel", 3, { + tables: [], + columnsByTable: new Map(), + databaseType: "oracle", + }); + + const snippet = items.find((item) => item.type === "snippet" && item.label === "select *"); + expect(snippet?.detail).toBe("SELECT *\nFROM table\nWHERE ROWNUM <= 100;"); + expect(snippet?.apply).toBe("SELECT *\nFROM ${table}\nWHERE ROWNUM <= 100;"); + }); + + it("preserves a customized built-in select body instead of replacing it with a dialect default", () => { + const customized = { ...BUILTIN_SELECT, body: "SELECT *\nFROM table\nLIMIT 7;" }; + const items = buildSnippetItemsForTest("sel", [customized], undefined, "oracle"); + + expect(items[0].detail).toBe("SELECT *\nFROM table\nLIMIT 7;"); + expect(items[0].apply).toBe("SELECT *\nFROM ${table}\nLIMIT 7;"); + }); + it("replaces placeholder words but preserves uppercase SQL keywords", () => { const items = buildSnippetItemsForTest("ct", [BUILTIN_CREATE_TABLE]); @@ -44,6 +112,34 @@ describe("buildSnippetItems", () => { expect(items[0].apply).toBe("UPDATE ${table}\nSET ${column} = ${value}\nWHERE ${condition};"); }); + it("uses ALTER TABLE UPDATE for ClickHouse", () => { + const items = buildSnippetItemsForTest("upd", [BUILTIN_UPDATE], undefined, "clickhouse"); + + expect(items[0].detail).toBe("ALTER TABLE table\nUPDATE column = value\nWHERE condition;"); + expect(items[0].apply).toBe("ALTER TABLE ${table}\nUPDATE ${column} = ${value}\nWHERE ${condition};"); + }); + + it.each([ + ["mysql", "ALTER TABLE table\nADD COLUMN column type;", "ALTER TABLE ${table}\nADD COLUMN ${column} ${type};"], + ["oracle", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["oceanbase-oracle", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["oscar", "ALTER TABLE table\nADD COLUMN column type;", "ALTER TABLE ${table}\nADD COLUMN ${column} ${type};"], + ["yashandb", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["xugu", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["dameng", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["iris", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["informix", "ALTER TABLE table\nADD (column type);", "ALTER TABLE ${table}\nADD (${column} ${type});"], + ["sqlserver", "ALTER TABLE table\nADD column type;", "ALTER TABLE ${table}\nADD ${column} ${type};"], + ["kingbase", "ALTER TABLE table\nADD column type;", "ALTER TABLE ${table}\nADD ${column} ${type};"], + ["cassandra", "ALTER TABLE table\nADD column type;", "ALTER TABLE ${table}\nADD ${column} ${type};"], + ["teradata", "ALTER TABLE table\nADD column type;", "ALTER TABLE ${table}\nADD ${column} ${type};"], + ] as const)("uses the %s ADD COLUMN syntax in the built-in alter-table snippet", (databaseType, detail, apply) => { + const items = buildSnippetItemsForTest("at", [BUILTIN_ALTER_TABLE], undefined, databaseType); + + expect(items[0].detail).toBe(detail); + expect(items[0].apply).toBe(apply); + }); + it("keeps keyword casing separate from editable placeholders", () => { const items = buildSnippetItemsForTest("sel", [BUILTIN_SELECT], "lower"); diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 31ce64efd8..c83cb2f9fe 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -53,6 +53,13 @@ export function effectiveDatabaseTypeForConnection(connection?: JdbcDialectConne return inferJdbcDialect(connection) ?? "jdbc"; } +export function sqlSnippetDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined { + // ASE uses T-SQL snippets, but mapping it globally to SQL Server would also + // enable incompatible SQL Server metadata and pagination behavior. + if (isJdbcAseProfile(connection)) return "sqlserver"; + return effectiveDatabaseTypeForConnection(connection); +} + export function tableStructureDatabaseTypeForConnection(connection?: JdbcDialectConnection): DatabaseType | undefined { if (!connection) return undefined; if (connection.db_type === "gbase" && !isGbase8sProfile(connection.driver_profile)) return "gbase"; @@ -113,13 +120,16 @@ export function codeMirrorSqlDialect(dbType: DatabaseType | undefined): "mysql" } export function codeMirrorSqlDialectForConnection(connection?: JdbcDialectConnection): "mysql" | "postgres" | "sqlserver" { - if (connection?.db_type === "jdbc") { - const explicitIdentity = [connection.driver_profile, connection.driver_label, connection.database_info?.productName].filter(Boolean).join("\n"); - if (JDBC_ASE_PROFILE_PATTERNS.some((pattern) => pattern.test(explicitIdentity))) return "sqlserver"; - } + if (isJdbcAseProfile(connection)) return "sqlserver"; return codeMirrorSqlDialect(effectiveDatabaseTypeForConnection(connection)); } +function isJdbcAseProfile(connection?: JdbcDialectConnection): boolean { + if (connection?.db_type !== "jdbc") return false; + const explicitIdentity = [connection.driver_profile, connection.driver_label, connection.database_info?.productName].filter(Boolean).join("\n"); + return JDBC_ASE_PROFILE_PATTERNS.some((pattern) => pattern.test(explicitIdentity)); +} + function isGbase8sProfile(driverProfile?: string): boolean { return driverProfile === "gbase8s"; } diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index a01dca32dc..b444976569 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -3,6 +3,9 @@ import type { DatabaseType, SqlSnippet } from "@/types/database"; import { buildMongoCompletionItemsFromContext, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion"; import { CLOUDFLARE_D1_COMMON_FUNCTION_NAMES } from "@/lib/sql/cloudflareD1"; import type { SqlObjectNavigationType } from "@/lib/sql/sqlNavigation"; +import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates"; + +export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates"; const SQL_KEYWORDS = [ "SELECT", @@ -778,119 +781,6 @@ function getFunctionDescriptions(t?: SqlCompletionTranslations): Map([ // Aggregate ["COUNT", ["expression"]], @@ -1446,7 +1336,7 @@ class SqlCompletionProvider { if (!pendingJoinKeyword && !context.exclusiveTableSuggestions && !context.exclusiveColumnSuggestions && !context.exclusiveRoutineSuggestions) { const snippets = this.databaseType === "manticoresearch" ? [...(this.input.snippets ?? DEFAULT_SQL_SNIPPETS), ...MANTICORESEARCH_SQL_SNIPPETS] : (this.input.snippets ?? DEFAULT_SQL_SNIPPETS); if (!preferReferencedColumns) { - this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase)); + this.items.push(...buildSnippetItems(context.prefix, snippets, this.input.keywordCase, this.databaseType)); } if (!preferReferencedColumns || context.suggestRoutines) { const functionItems = context.dataTypeContext ? [] : buildFunctionSnippetItems(context.prefix, getFunctionDescriptions(this.t), this.databaseType); @@ -1463,6 +1353,7 @@ class SqlCompletionProvider { context.prefix, MANTICORESEARCH_SQL_SNIPPETS.filter((snippet) => snippet.id === "builtin-manticore-call-pq"), this.input.keywordCase, + this.databaseType, ), ); } @@ -3250,9 +3141,9 @@ function applyBuiltinSnippetKeywordCase(snippet: SqlSnippet, text: string, keywo const BUILTIN_SNIPPET_PLACEHOLDER_RE = /\b(idx_name|left_column|right_column|columns|values|condition|column|default|value|name|type|table)\b/g; -function applyBuiltinSnippetPlaceholders(snippet: SqlSnippet): string { - if (!shouldFormatBuiltinSnippet(snippet)) return snippet.body; - return snippet.body.replace(BUILTIN_SNIPPET_PLACEHOLDER_RE, (match) => `\${${match}}`); +function applyBuiltinSnippetPlaceholders(snippet: SqlSnippet, body = snippet.body): string { + if (!shouldFormatBuiltinSnippet(snippet)) return body; + return body.replace(BUILTIN_SNIPPET_PLACEHOLDER_RE, (match) => `\${${match}}`); } function buildPreferredKeywordItems(prefix: string, keywords: string[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] { @@ -4108,11 +3999,11 @@ function singularTableName(name: string): string { return lower; } -export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] { - return buildSnippetItems(prefix, snippets, keywordCase); +export function buildSnippetItemsForTest(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase, databaseType?: DatabaseType): SqlCompletionItem[] { + return buildSnippetItems(prefix, snippets, keywordCase, databaseType); } -function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase): SqlCompletionItem[] { +function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: SqlKeywordCase, databaseType?: DatabaseType): SqlCompletionItem[] { if (!prefix) return []; return snippets .filter((snippet) => { @@ -4131,8 +4022,9 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?: const baseBoost = matchesByPrefix ? 4000 : 0; // Placeholder replacement runs on the original (UPPER-case) body first, // then keyword casing is applied to both variants uniformly. - const body = applyBuiltinSnippetKeywordCase(snippet, snippet.body, keywordCase); - const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet), keywordCase); + const resolvedBody = resolveSqlSnippetBodyForDatabase(snippet, databaseType); + const body = applyBuiltinSnippetKeywordCase(snippet, resolvedBody, keywordCase); + const apply = applyBuiltinSnippetKeywordCase(snippet, applyBuiltinSnippetPlaceholders(snippet, resolvedBody), keywordCase); return { label: snippet.label, type: "snippet" as const, diff --git a/apps/desktop/src/lib/sql/sqlSnippetTemplates.ts b/apps/desktop/src/lib/sql/sqlSnippetTemplates.ts new file mode 100644 index 0000000000..942b01a286 --- /dev/null +++ b/apps/desktop/src/lib/sql/sqlSnippetTemplates.ts @@ -0,0 +1,215 @@ +import type { DatabaseType, SqlSnippet } from "@/types/database"; + +const DEFAULT_SELECT_ROW_LIMIT = 100; + +/** + * Built-in SQL snippets shared by database types. + * + * Database-specific bodies are resolved at completion time so the stored + * defaults remain portable and existing user-customized bodies stay intact. + */ +export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = [ + { + id: "builtin-sel", + label: "select *", + prefix: "sel", + body: `SELECT *\nFROM table\nLIMIT ${DEFAULT_SELECT_ROW_LIMIT};`, + enabled: true, + }, + { + id: "builtin-ins", + label: "insert into", + prefix: "ins", + body: "INSERT INTO table (columns)\nVALUES (values);", + enabled: true, + }, + { + id: "builtin-upd", + label: "update set", + prefix: "upd", + body: "UPDATE table\nSET column = value\nWHERE condition;", + enabled: true, + }, + { + id: "builtin-cte", + label: "common table expression", + prefix: "cte", + body: "WITH name AS (\n SELECT columns\n FROM table\n)\nSELECT *\nFROM name;", + enabled: true, + }, + { + id: "builtin-join", + label: "join", + prefix: "join", + body: "JOIN table ON left_column = right_column", + enabled: true, + }, + { + id: "builtin-case", + label: "case when", + prefix: "case", + body: "CASE\n WHEN condition THEN value\n ELSE default\nEND", + enabled: true, + }, + { + id: "builtin-ct", + label: "create table", + prefix: "ct", + body: "CREATE TABLE table (\n column type\n);", + enabled: true, + }, + { + id: "builtin-ex", + label: "exists", + prefix: "ex", + body: "EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)", + enabled: true, + }, + { + id: "builtin-nex", + label: "not exists", + prefix: "nex", + body: "NOT EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)", + enabled: true, + }, + { + id: "builtin-at", + label: "alter table add column", + prefix: "at", + body: "ALTER TABLE table\nADD COLUMN column type;", + enabled: true, + }, + { + id: "builtin-ci", + label: "create index", + prefix: "ci", + body: "CREATE INDEX idx_name\nON table (column);", + enabled: true, + }, +]; + +/** Additional snippets only exposed by the Manticore Search completion path. */ +export const MANTICORESEARCH_SQL_SNIPPETS: SqlSnippet[] = [ + { + id: "builtin-manticore-match", + label: "match query", + prefix: "match", + body: "MATCH('query')", + }, + { + id: "builtin-manticore-facet", + label: "facet", + prefix: "facet", + body: "FACET column", + }, + { + id: "builtin-manticore-show-meta", + label: "show meta", + prefix: "m", + body: "SHOW META;", + }, + { + id: "builtin-manticore-show-tables", + label: "show tables", + prefix: "tab", + body: "SHOW TABLES;", + }, + { + id: "builtin-manticore-call-pq", + label: "call pq", + prefix: "p", + body: "CALL PQ ('pq', ('{\"title\":\"query\"}'));", + }, +]; + +type BuiltinSqlSnippetBodyBuilder = (databaseType?: DatabaseType) => string; + +interface BuiltinSqlSnippetRule { + defaultBody: string; + buildBody: BuiltinSqlSnippetBodyBuilder; +} + +type SelectSnippetLimitStyle = "limit" | "top" | "first" | "fetch-first" | "rows" | "rownum" | "unbounded"; + +const SELECT_SNIPPET_LIMIT_STYLE_BY_DATABASE: Partial> = { + oracle: "rownum", + "oceanbase-oracle": "rownum", + oscar: "rownum", + dameng: "fetch-first", + db2: "fetch-first", + sqlserver: "top", + access: "top", + iris: "top", + teradata: "top", + informix: "first", + firebird: "rows", + // An unknown JDBC driver may expose any SQL dialect. Known JDBC profiles are + // converted to their effective database type before reaching the editor. + jdbc: "unbounded", +}; + +const PARENTHESIZED_ADD_COLUMN_DATABASES = new Set(["oracle", "oceanbase-oracle", "yashandb", "xugu", "dameng", "iris", "informix"]); +const ADD_COLUMN_WITHOUT_COLUMN_KEYWORD_DATABASES = new Set(["sqlserver", "kingbase", "cassandra", "teradata"]); + +function buildSelectSnippetBody(databaseType?: DatabaseType): string { + const style = databaseType ? (SELECT_SNIPPET_LIMIT_STYLE_BY_DATABASE[databaseType] ?? "limit") : "limit"; + switch (style) { + case "top": + return `SELECT TOP ${DEFAULT_SELECT_ROW_LIMIT} *\nFROM table;`; + case "first": + return `SELECT FIRST ${DEFAULT_SELECT_ROW_LIMIT} *\nFROM table;`; + case "fetch-first": + return `SELECT *\nFROM table\nFETCH FIRST ${DEFAULT_SELECT_ROW_LIMIT} ROWS ONLY;`; + case "rows": + return `SELECT *\nFROM table\nROWS ${DEFAULT_SELECT_ROW_LIMIT};`; + case "rownum": + return `SELECT *\nFROM table\nWHERE ROWNUM <= ${DEFAULT_SELECT_ROW_LIMIT};`; + case "unbounded": + return "SELECT *\nFROM table;"; + case "limit": + return `SELECT *\nFROM table\nLIMIT ${DEFAULT_SELECT_ROW_LIMIT};`; + } +} + +function buildUpdateSnippetBody(databaseType?: DatabaseType): string { + if (databaseType === "clickhouse") { + return "ALTER TABLE table\nUPDATE column = value\nWHERE condition;"; + } + return "UPDATE table\nSET column = value\nWHERE condition;"; +} + +function buildAlterTableAddColumnSnippetBody(databaseType?: DatabaseType): string { + if (databaseType && PARENTHESIZED_ADD_COLUMN_DATABASES.has(databaseType)) { + return "ALTER TABLE table\nADD (column type);"; + } + if (databaseType && ADD_COLUMN_WITHOUT_COLUMN_KEYWORD_DATABASES.has(databaseType)) { + return "ALTER TABLE table\nADD column type;"; + } + return "ALTER TABLE table\nADD COLUMN column type;"; +} + +const BUILTIN_SQL_SNIPPET_RULES = new Map( + DEFAULT_SQL_SNIPPETS.map((snippet) => [ + snippet.id, + { + defaultBody: snippet.body, + buildBody: () => snippet.body, + }, + ]), +); + +function registerBuiltinSqlSnippetRule(id: string, buildBody: BuiltinSqlSnippetBodyBuilder): void { + const rule = BUILTIN_SQL_SNIPPET_RULES.get(id); + if (!rule) throw new Error(`Unknown built-in SQL snippet: ${id}`); + BUILTIN_SQL_SNIPPET_RULES.set(id, { defaultBody: rule.defaultBody, buildBody }); +} + +registerBuiltinSqlSnippetRule("builtin-sel", buildSelectSnippetBody); +registerBuiltinSqlSnippetRule("builtin-upd", buildUpdateSnippetBody); +registerBuiltinSqlSnippetRule("builtin-at", buildAlterTableAddColumnSnippetBody); + +export function resolveSqlSnippetBodyForDatabase(snippet: SqlSnippet, databaseType?: DatabaseType): string { + const rule = BUILTIN_SQL_SNIPPET_RULES.get(snippet.id); + if (!rule || snippet.body !== rule.defaultBody) return snippet.body; + return rule.buildBody(databaseType); +} diff --git a/packages/app-tests/jdbcDialect.test.ts b/packages/app-tests/jdbcDialect.test.ts index 8dfb9f1694..f29043564d 100644 --- a/packages/app-tests/jdbcDialect.test.ts +++ b/packages/app-tests/jdbcDialect.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test } from "vitest"; -import { codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection, inferJdbcDialect } from "../../apps/desktop/src/lib/database/jdbcDialect.ts"; +import { codeMirrorSqlDialectForConnection, effectiveDatabaseTypeForConnection, inferJdbcDialect, sqlSnippetDatabaseTypeForConnection } from "../../apps/desktop/src/lib/database/jdbcDialect.ts"; test("infers GoldenDB for generic JDBC connections", () => { assert.equal( @@ -38,6 +38,7 @@ test("uses SQL Server editor syntax for ASE without changing its effective JDBC for (const connection of aseConnections) { assert.equal(codeMirrorSqlDialectForConnection(connection), "sqlserver"); + assert.equal(sqlSnippetDatabaseTypeForConnection(connection), "sqlserver"); assert.equal(effectiveDatabaseTypeForConnection(connection), "jdbc"); } }); @@ -54,5 +55,6 @@ test("keeps non-ASE jConnect profiles on generic JDBC syntax", () => { }; assert.equal(codeMirrorSqlDialectForConnection(iqConnection), "mysql"); + assert.equal(sqlSnippetDatabaseTypeForConnection(iqConnection), "jdbc"); assert.equal(effectiveDatabaseTypeForConnection(iqConnection), "jdbc"); });