From 0ff3461815bc9986c2279bec8fde4c9fd9b6604c Mon Sep 17 00:00:00 2001 From: lxk955 <164191365+lxk955@users.noreply.github.com> Date: Mon, 19 Oct 2026 11:58:32 +0800 Subject: [PATCH] fix(sidebar): scope trigger refresh and support firebird trigger actions --- .../sidebar/SidebarTreeRuntimeHost.vue | 16 +-- .../__tests__/database/jdbcDialect.spec.ts | 21 ++++ .../database/triggerToggleSql.spec.ts | 20 +++ .../sidebar/sidebarObjectGroupRouting.spec.ts | 30 +++++ apps/desktop/src/lib/database/jdbcDialect.ts | 1 + .../src/lib/database/triggerToggleSql.ts | 7 +- .../lib/sidebar/databaseObjectTemplates.ts | 10 ++ .../lib/sidebar/firebirdObjectTemplates.ts | 88 +++++++++++++ .../src/lib/sidebar/mysqlObjectTemplates.ts | 2 +- apps/desktop/src/lib/table/tableTree.ts | 1 + apps/desktop/src/stores/connectionStore.ts | 6 +- .../app-tests/firebirdObjectTemplates.test.ts | 118 ++++++++++++++++++ 12 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 apps/desktop/src/lib/sidebar/databaseObjectTemplates.ts create mode 100644 apps/desktop/src/lib/sidebar/firebirdObjectTemplates.ts create mode 100644 packages/app-tests/firebirdObjectTemplates.test.ts diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue index 6e492937e3..4395398e15 100644 --- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue +++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue @@ -118,7 +118,7 @@ import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms"; import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql"; import { canViewDatabaseObjectDependencies, databaseDependencyProviderFor } from "@/lib/database/databaseObjectDependencies"; import { elasticsearchClearIndexPreview, isElasticsearchClearConfirmed, isElasticsearchIndexPattern } from "@/lib/sidebar/elasticsearchIndexActions"; -import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates"; +import { sidebarObjectTemplateForGroup } from "@/lib/sidebar/databaseObjectTemplates"; import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate, insertTemplatePrimaryKeyConflicts } from "@/lib/table/tableSqlTemplates"; import SqlTemplateInsertRowsDialog from "@/components/sidebar/SqlTemplateInsertRowsDialog.vue"; import { joinExportedDdls } from "@/lib/export/ddlExport"; @@ -964,7 +964,7 @@ async function toggle(requestId = beginNavigationRequest()) { // Keep the click path aligned with every object-group definition. In // particular, schema-level trigger/type groups have no tableName, so they // must use the generic object loader rather than the table-trigger loader. - const databaseObjectGroup = !!objectTypesForGroupNode(node.type); + const databaseObjectGroup = !!objectTypesForGroupNode(node.type) && !node.tableName; const needsConfiguredTablePageDrain = node.type === "group-tables" && connectionStore.getConfig(node.connectionId || "")?.sidebar_auto_load_all_tables === true && hasTableTreeLoadMore(node.children ?? []); if (databaseObjectGroup && connectionStore.canUseLoadedTreeNodeToggle(node) && !needsConfiguredTablePageDrain) { node.isExpanded = !node.isExpanded; @@ -5688,10 +5688,10 @@ async function confirmDeleteHBaseTable() { } } -function createMysqlObjectTemplate() { +function createSidebarObjectTemplate() { const node = activeNode.value; if (!node.connectionId || !node.database) return; - const template = mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node); + const template = sidebarObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node); if (!template) return; connectionStore.activeConnectionId = node.connectionId; const tabId = queryStore.createTab(node.connectionId, node.database, t(template.titleKey), "query", node.schema, undefined, node.catalog); @@ -7752,11 +7752,11 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea appendTableVGroupContainerItems(node, items); // 9. Group Labels (group-columns, group-tables, etc.) if (isGroupLabel(node)) { - const mysqlObjectTemplate = node.connectionId ? mysqlObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null; + const objectTemplate = node.connectionId ? sidebarObjectTemplateForGroup(connectionStore.getConfig(node.connectionId), node) : null; const hasMongoCreateIndexAction = node.type === "group-indexes" && canCreateMongoIndex.value; const hasMongoDropAllIndexesAction = node.type === "group-indexes" && canDropAllMongoIndexes.value; const canCreateGroupView = node.type === "group-views" && !!node.connectionId && !!node.database && !["neo4j", "nebula"].includes(currentDatabaseType() || ""); - const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || canCreateGroupView || !!mysqlObjectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction; + const hasGroupAction = (node.type === "group-tables" && canCreateTable.value) || canCreateGroupView || !!objectTemplate || hasMongoCreateIndexAction || hasMongoDropAllIndexesAction; const canLoadAllObjectGroup = node.type === "group-tables" || node.type === "group-dolt-system-tables" || node.type === "group-views" || node.type === "group-materialized-views"; if (node.type === "group-tables" && canCreateTable.value) { items.push({ label: t("contextMenu.createTable"), action: createTable, icon: Plus }); @@ -7773,8 +7773,8 @@ function buildObjectGroupSidebarMenu(context: SidebarMenuFactoryContext): boolea if (node.type === "group-events" && node.connectionId && node.database) { items.push({ label: t("contextMenu.createEvent"), action: openMysqlEventCreateEditor, icon: Plus }); } - if (mysqlObjectTemplate) { - items.push({ label: t(mysqlObjectTemplate.titleKey), action: createMysqlObjectTemplate, icon: Plus }); + if (objectTemplate) { + items.push({ label: t(objectTemplate.titleKey), action: createSidebarObjectTemplate, icon: Plus }); } if (hasMongoCreateIndexAction) { items.push({ label: t("contextMenu.createMongoIndex"), action: openCreateMongoIndexDialog, icon: Plus }); diff --git a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts index 3014ef4a21..8c15f33789 100644 --- a/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/jdbcDialect.spec.ts @@ -85,6 +85,27 @@ describe("jdbc dialect inference", () => { ).toBe("iris"); }); + it("detects Firebird JDBC connections", () => { + expect( + inferJdbcDialect({ + db_type: "jdbc", + connection_string: "jdbc:firebirdsql://localhost:3050/data/test.fdb", + }), + ).toBe("firebird"); + expect( + inferJdbcDialect({ + db_type: "jdbc", + jdbc_driver_class: "org.firebirdsql.jdbc.FBDriver", + }), + ).toBe("firebird"); + expect( + inferJdbcDialect({ + db_type: "jdbc", + driver_profile: "firebird", + }), + ).toBe("firebird"); + }); + it("uses JDBC driver profiles when inferring dialect", () => { expect( inferJdbcDialect({ diff --git a/apps/desktop/src/lib/__tests__/database/triggerToggleSql.spec.ts b/apps/desktop/src/lib/__tests__/database/triggerToggleSql.spec.ts index 72cf2d0472..5ca939835a 100644 --- a/apps/desktop/src/lib/__tests__/database/triggerToggleSql.spec.ts +++ b/apps/desktop/src/lib/__tests__/database/triggerToggleSql.spec.ts @@ -17,6 +17,7 @@ describe("triggerToggleSql", () => { expect(supportsTriggerToggle("gaussdb")).toBe(true); expect(supportsTriggerToggle("kwdb")).toBe(true); expect(supportsTriggerToggle("xugu")).toBe(true); + expect(supportsTriggerToggle("firebird")).toBe(true); }); it("returns false for unsupported database types or undefined", () => { @@ -147,6 +148,25 @@ describe("triggerToggleSql", () => { } }); + it("generates Firebird trigger enable/disable SQL", () => { + expect( + buildTriggerToggleSql({ + databaseType: "firebird", + tableName: "orders", + triggerName: "trg_orders_audit", + enabled: true, + }), + ).toBe('ALTER TRIGGER "trg_orders_audit" ACTIVE;'); + + expect( + buildTriggerToggleSql({ + databaseType: "firebird", + triggerName: 'trg_"orders"_audit', + enabled: false, + }), + ).toBe('ALTER TRIGGER "trg_""orders""_audit" INACTIVE;'); + }); + it("returns null for PostgreSQL when tableName is missing", () => { expect( buildTriggerToggleSql({ diff --git a/apps/desktop/src/lib/__tests__/sidebar/sidebarObjectGroupRouting.spec.ts b/apps/desktop/src/lib/__tests__/sidebar/sidebarObjectGroupRouting.spec.ts index a36cd3575c..2a01041cf6 100644 --- a/apps/desktop/src/lib/__tests__/sidebar/sidebarObjectGroupRouting.spec.ts +++ b/apps/desktop/src/lib/__tests__/sidebar/sidebarObjectGroupRouting.spec.ts @@ -129,6 +129,36 @@ describe("sidebar object-group routing", () => { }); }); + it("refreshes table trigger groups using listTriggers instead of loading database objects", async () => { + const listObjects = vi.fn<() => Promise>().mockResolvedValue([]); + const listTriggers = vi.fn<() => Promise>().mockResolvedValue([ + { + name: "trg_orders_audit", + timing: "AFTER", + event: "UPDATE", + }, + ]); + const { connection, store } = await createStore({ listObjects, listTriggers }); + const tableTriggerGroup: TreeNode = { + ...objectGroup("group-triggers", `${connection.id}:app:app:orders:__triggers`), + tableName: "orders", + }; + store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [tableTriggerGroup] }]; + const storedTriggerGroup = store.treeNodes[0].children![0]; + + await store.refreshTreeNode(storedTriggerGroup); + + expect(listTriggers).toHaveBeenCalledWith(connection.id, "app", "app", "orders", undefined); + expect(listObjects).not.toHaveBeenCalled(); + expect(storedTriggerGroup.children).toHaveLength(1); + expect(storedTriggerGroup.children![0]).toMatchObject({ + label: "trg_orders_audit (AFTER UPDATE)", + objectName: "trg_orders_audit", + tableName: "orders", + type: "trigger", + }); + }); + it("preserves SQL Server table trigger status metadata across refreshes", async () => { const listObjects = vi.fn<() => Promise>().mockResolvedValue([]); const listTriggers = vi.fn<() => Promise>(); diff --git a/apps/desktop/src/lib/database/jdbcDialect.ts b/apps/desktop/src/lib/database/jdbcDialect.ts index 84084c2b13..e34bc532f9 100644 --- a/apps/desktop/src/lib/database/jdbcDialect.ts +++ b/apps/desktop/src/lib/database/jdbcDialect.ts @@ -43,6 +43,7 @@ const JDBC_DIALECT_MATCHERS: Array<{ type: DatabaseType; patterns: RegExp[] }> = { type: "sqlite", patterns: [/jdbc:sqlite:/i, /sqlite/i] }, { type: "db2", patterns: [/jdbc:db2:/i, /\bdb2\b/i] }, { type: "informix", patterns: [/jdbc:informix/i, /informix/i] }, + { type: "firebird", patterns: [/jdbc:firebird(?:sql)?:/i, /firebird/i, /org\.firebirdsql\./i] }, // CacheDB.jar (legacy Caché driver) carries none of the intersystems URL or // class-name markers, so match the jar file name / driver label directly. { type: "iris", patterns: [/jdbc:(?:iris|cache):/i, /com\.intersystems\.jdbc\.(?:IRIS|Cache)Driver/i, /intersystems-jdbc/i, /\bcache(?:db)?\b/i] }, diff --git a/apps/desktop/src/lib/database/triggerToggleSql.ts b/apps/desktop/src/lib/database/triggerToggleSql.ts index e45f00cfc3..70af1768fd 100644 --- a/apps/desktop/src/lib/database/triggerToggleSql.ts +++ b/apps/desktop/src/lib/database/triggerToggleSql.ts @@ -8,7 +8,7 @@ export interface TriggerToggleSqlOptions { enabled: boolean; } -const SUPPORTED_TRIGGER_TOGGLE_DATABASES = new Set(["sqlserver", "oracle", "oceanbase-oracle", "dameng", "postgres", "kingbase", "opengauss", "vastbase", "highgo", "uxdb", "gaussdb", "kwdb", "xugu"]); +const SUPPORTED_TRIGGER_TOGGLE_DATABASES = new Set(["sqlserver", "oracle", "oceanbase-oracle", "dameng", "postgres", "kingbase", "opengauss", "vastbase", "highgo", "uxdb", "gaussdb", "kwdb", "xugu", "firebird"]); export function supportsTriggerToggle(databaseType?: DatabaseType): boolean { if (!databaseType) return false; @@ -64,6 +64,11 @@ export function buildTriggerToggleSql(options: TriggerToggleSqlOptions): string return `ALTER TABLE ${quotedTable} ${stateKeyword} TRIGGER ${quotedTrigger};`; } + case "firebird": { + const quotedTrigger = quoteDouble(cleanTrigger); + return `ALTER TRIGGER ${quotedTrigger} ${enabled ? "ACTIVE" : "INACTIVE"};`; + } + default: return null; } diff --git a/apps/desktop/src/lib/sidebar/databaseObjectTemplates.ts b/apps/desktop/src/lib/sidebar/databaseObjectTemplates.ts new file mode 100644 index 0000000000..a62db8b3c8 --- /dev/null +++ b/apps/desktop/src/lib/sidebar/databaseObjectTemplates.ts @@ -0,0 +1,10 @@ +import type { TreeNode } from "@/types/database"; +import type { JdbcDialectConnection } from "@/lib/database/jdbcDialect"; +import { mysqlObjectTemplateForGroup, type MysqlObjectTemplate } from "@/lib/sidebar/mysqlObjectTemplates"; +import { firebirdObjectTemplateForGroup, type FirebirdObjectTemplate } from "@/lib/sidebar/firebirdObjectTemplates"; + +export type SidebarObjectTemplate = MysqlObjectTemplate | FirebirdObjectTemplate; + +export function sidebarObjectTemplateForGroup(connection: JdbcDialectConnection | undefined, node: Pick): SidebarObjectTemplate | null { + return mysqlObjectTemplateForGroup(connection, node) || firebirdObjectTemplateForGroup(connection, node); +} diff --git a/apps/desktop/src/lib/sidebar/firebirdObjectTemplates.ts b/apps/desktop/src/lib/sidebar/firebirdObjectTemplates.ts new file mode 100644 index 0000000000..eb5892e7c9 --- /dev/null +++ b/apps/desktop/src/lib/sidebar/firebirdObjectTemplates.ts @@ -0,0 +1,88 @@ +import type { TreeNode, TreeNodeType } from "@/types/database"; +import { effectiveDatabaseTypeForConnection, type JdbcDialectConnection } from "@/lib/database/jdbcDialect"; + +export type FirebirdObjectTemplateKind = "procedure" | "function" | "trigger"; + +export interface FirebirdObjectTemplate { + kind: FirebirdObjectTemplateKind; + titleKey: "contextMenu.createProcedure" | "contextMenu.createFunction" | "contextMenu.createTrigger"; + sql: string; +} + +type FirebirdTemplateConnection = JdbcDialectConnection; +type FirebirdTemplateNode = Pick; + +const templateKindByGroup: Partial> = { + "group-procedures": "procedure", + "group-functions": "function", + "group-triggers": "trigger", +}; + +export function supportsFirebirdObjectTemplates(connection?: FirebirdTemplateConnection): boolean { + return effectiveDatabaseTypeForConnection(connection) === "firebird"; +} + +export function firebirdObjectTemplateForGroup(connection: FirebirdTemplateConnection | undefined, node: FirebirdTemplateNode): FirebirdObjectTemplate | null { + if (!supportsFirebirdObjectTemplates(connection) || !node.database) return null; + const kind = templateKindByGroup[node.type]; + if (!kind) return null; + return buildFirebirdObjectTemplate(kind, node.tableName); +} + +export function buildFirebirdObjectTemplate(kind: FirebirdObjectTemplateKind, tableName?: string): FirebirdObjectTemplate { + if (kind === "procedure") { + const name = "new_procedure"; + return { + kind, + titleKey: "contextMenu.createProcedure", + sql: `CREATE OR ALTER PROCEDURE ${quoteFirebirdIdentifier(name)} +AS +BEGIN + /* Procedure body */ +END;`, + }; + } + + if (kind === "function") { + const name = "new_function"; + return { + kind, + titleKey: "contextMenu.createFunction", + sql: `CREATE OR ALTER FUNCTION ${quoteFirebirdIdentifier(name)}() +RETURNS INTEGER +AS +BEGIN + RETURN 0; +END;`, + }; + } + + const name = "new_trigger"; + if (tableName) { + return { + kind, + titleKey: "contextMenu.createTrigger", + sql: `CREATE OR ALTER TRIGGER ${quoteFirebirdIdentifier(name)} FOR ${quoteFirebirdIdentifier(tableName)} +ACTIVE BEFORE INSERT POSITION 0 +AS +BEGIN + /* Trigger body */ +END;`, + }; + } + + return { + kind, + titleKey: "contextMenu.createTrigger", + sql: `CREATE OR ALTER TRIGGER ${quoteFirebirdIdentifier(name)} +ACTIVE ON CONNECT POSITION 0 +AS +BEGIN + /* Trigger body */ +END;`, + }; +} + +export function quoteFirebirdIdentifier(identifier: string): string { + return `"${identifier.replaceAll('"', '""')}"`; +} diff --git a/apps/desktop/src/lib/sidebar/mysqlObjectTemplates.ts b/apps/desktop/src/lib/sidebar/mysqlObjectTemplates.ts index 5014d2d642..ef0139d50a 100644 --- a/apps/desktop/src/lib/sidebar/mysqlObjectTemplates.ts +++ b/apps/desktop/src/lib/sidebar/mysqlObjectTemplates.ts @@ -8,7 +8,7 @@ export interface MysqlObjectTemplate { sql: string; } -type MysqlTemplateConnection = Pick; +type MysqlTemplateConnection = Partial>; type MysqlTemplateNode = Pick; const templateKindByGroup: Partial> = { diff --git a/apps/desktop/src/lib/table/tableTree.ts b/apps/desktop/src/lib/table/tableTree.ts index c0e247d920..a503794210 100644 --- a/apps/desktop/src/lib/table/tableTree.ts +++ b/apps/desktop/src/lib/table/tableTree.ts @@ -897,6 +897,7 @@ export function buildObjectGroupPlaceholderNodes({ } export function objectGroupRefreshParentId(node: TreeNode): string | null { + if (node.tableName) return null; if (!objectGroupNodeTypes.has(node.type)) return null; const suffixStart = node.id.lastIndexOf(":__"); if (suffixStart < 0) return null; diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts index 70a3a411f5..d358d1e6b5 100644 --- a/apps/desktop/src/stores/connectionStore.ts +++ b/apps/desktop/src/stores/connectionStore.ts @@ -8069,7 +8069,7 @@ export const useConnectionStore = defineStore("connection", () => { } invalidateCompletionCachesForNode(node); invalidateMetadataCachesForNode(node, options); - if (objectTypesForGroupNode(node.type)) { + if (objectTypesForGroupNode(node.type) && !node.tableName) { clearLoadedChildrenCache(node.id, { deletePersisted: false }); await loadObjectGroupChildren(node, { force: true }); await invalidateSidebarTableSearchIndexesForTreeRefresh(node); @@ -8145,7 +8145,7 @@ export const useConnectionStore = defineStore("connection", () => { async function refreshTreeNodeForTableNameFilter(node: TreeNode, scopeKey: string, revision: number) { invalidateMetadataCachesForNode(node); - if (objectTypesForGroupNode(node.type)) { + if (objectTypesForGroupNode(node.type) && !node.tableName) { clearLoadedChildrenCache(node.id); await loadObjectGroupChildren(node, { force: true, @@ -10124,7 +10124,7 @@ export const useConnectionStore = defineStore("connection", () => { if (node.children) await refreshNodes(node.children); continue; } - if (objectTypesForGroupNode(node.type)) { + if (objectTypesForGroupNode(node.type) && !node.tableName) { if (node.connectionId && connectedIds.value.has(node.connectionId)) { clearLoadedChildrenCache(node.id); if (node.isExpanded) { diff --git a/packages/app-tests/firebirdObjectTemplates.test.ts b/packages/app-tests/firebirdObjectTemplates.test.ts new file mode 100644 index 0000000000..c14423b2cd --- /dev/null +++ b/packages/app-tests/firebirdObjectTemplates.test.ts @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import { test } from "vitest"; +import { buildFirebirdObjectTemplate, firebirdObjectTemplateForGroup, quoteFirebirdIdentifier, supportsFirebirdObjectTemplates } from "../../apps/desktop/src/lib/sidebar/firebirdObjectTemplates.ts"; +import { sidebarObjectTemplateForGroup } from "../../apps/desktop/src/lib/sidebar/databaseObjectTemplates.ts"; +import { executableStatementRanges } from "../../apps/desktop/src/lib/sql/sqlStatementRanges.ts"; +import type { ConnectionConfig, TreeNode } from "../../apps/desktop/src/types/database.ts"; + +function connection(dbType: ConnectionConfig["db_type"], driverProfile?: string): ConnectionConfig { + return { + id: "conn-1", + name: "Test", + db_type: dbType, + driver_profile: driverProfile, + host: "127.0.0.1", + port: 3050, + username: "sysdba", + password: "", + }; +} + +function group(type: TreeNode["type"], overrides: Partial = {}): TreeNode { + return { + id: `conn-1:app:${type}`, + label: type, + type, + connectionId: "conn-1", + database: "app", + children: [], + ...overrides, + }; +} + +test("Firebird object templates are supported for Firebird connections", () => { + assert.equal(supportsFirebirdObjectTemplates(connection("firebird")), true); + assert.equal(supportsFirebirdObjectTemplates(connection("jdbc", "firebird")), true); + assert.equal(supportsFirebirdObjectTemplates({ db_type: "jdbc", connection_string: "jdbc:firebirdsql://localhost:3050/test.fdb" }), true); + assert.equal(supportsFirebirdObjectTemplates({ db_type: "jdbc", jdbc_driver_class: "org.firebirdsql.jdbc.FBDriver" }), true); + assert.equal(supportsFirebirdObjectTemplates(connection("mysql")), false); + assert.equal(supportsFirebirdObjectTemplates(connection("postgres")), false); +}); + +test("only supported Firebird object groups expose creation templates", () => { + const fb = connection("firebird"); + assert.equal(firebirdObjectTemplateForGroup(fb, group("group-procedures"))?.kind, "procedure"); + assert.equal(firebirdObjectTemplateForGroup(fb, group("group-functions"))?.kind, "function"); + assert.equal(firebirdObjectTemplateForGroup(fb, group("group-triggers"))?.kind, "trigger"); + assert.equal(firebirdObjectTemplateForGroup(fb, group("group-views")), null); + assert.equal(firebirdObjectTemplateForGroup(fb, group("group-functions", { database: "" })), null); +}); + +test("procedure and function templates use Firebird CREATE OR ALTER syntax", () => { + assert.equal( + buildFirebirdObjectTemplate("procedure").sql, + `CREATE OR ALTER PROCEDURE "new_procedure" +AS +BEGIN + /* Procedure body */ +END;`, + ); + assert.equal( + buildFirebirdObjectTemplate("function").sql, + `CREATE OR ALTER FUNCTION "new_function"() +RETURNS INTEGER +AS +BEGIN + RETURN 0; +END;`, + ); +}); + +test("DBX parses every Firebird template as one executable statement", () => { + for (const kind of ["procedure", "function"] as const) { + const ranges = executableStatementRanges(buildFirebirdObjectTemplate(kind).sql, "firebird"); + assert.equal(ranges.length, 1, kind); + assert.match(ranges[0].sql, new RegExp(`^CREATE OR ALTER ${kind.toUpperCase()}`)); + } + const tableTriggerRanges = executableStatementRanges(buildFirebirdObjectTemplate("trigger", "A_DETAIL").sql, "firebird"); + assert.equal(tableTriggerRanges.length, 1, "table trigger"); + assert.match(tableTriggerRanges[0].sql, /^CREATE OR ALTER TRIGGER/); + + const dbTriggerRanges = executableStatementRanges(buildFirebirdObjectTemplate("trigger").sql, "firebird"); + assert.equal(dbTriggerRanges.length, 1, "database trigger"); + assert.match(dbTriggerRanges[0].sql, /^CREATE OR ALTER TRIGGER/); +}); + +test("trigger templates use table context when available and ON CONNECT for database triggers", () => { + const tableTrigger = buildFirebirdObjectTemplate("trigger", 'order"items'); + assert.match(tableTrigger.sql, /FOR "order""items"/); + assert.match(tableTrigger.sql, /ACTIVE BEFORE INSERT POSITION 0/); + + const dbTrigger = buildFirebirdObjectTemplate("trigger"); + assert.match(dbTrigger.sql, /ACTIVE ON CONNECT POSITION 0/); + + const groupTemplate = firebirdObjectTemplateForGroup(connection("firebird"), group("group-triggers", { tableName: "A_DETAIL" })); + assert.equal(groupTemplate?.kind, "trigger"); + assert.match(groupTemplate?.sql || "", /FOR "A_DETAIL"/); +}); + +test("quoteFirebirdIdentifier properly escapes quotes", () => { + assert.equal(quoteFirebirdIdentifier("table_name"), '"table_name"'); + assert.equal(quoteFirebirdIdentifier('my"table"name'), '"my""table""name"'); +}); + +test("sidebarObjectTemplateForGroup routes both MySQL and Firebird templates", () => { + const mysql = connection("mysql", "mysql"); + const fb = connection("firebird"); + const pg = connection("postgres"); + + const mysqlTpl = sidebarObjectTemplateForGroup(mysql, group("group-triggers", { tableName: "orders" })); + assert.equal(mysqlTpl?.kind, "trigger"); + assert.match(mysqlTpl?.sql || "", /DELIMITER/); + + const fbTpl = sidebarObjectTemplateForGroup(fb, group("group-triggers", { tableName: "ORDERS" })); + assert.equal(fbTpl?.kind, "trigger"); + assert.match(fbTpl?.sql || "", /CREATE OR ALTER TRIGGER "new_trigger" FOR "ORDERS"/); + + assert.equal(sidebarObjectTemplateForGroup(pg, group("group-triggers", { tableName: "orders" })), null); +});