diff --git a/packages/server/src/api/routes/tests/row.spec.ts b/packages/server/src/api/routes/tests/row.spec.ts index 0c6c54e52f..9df8ab2ee2 100644 --- a/packages/server/src/api/routes/tests/row.spec.ts +++ b/packages/server/src/api/routes/tests/row.spec.ts @@ -864,7 +864,7 @@ describe.each([ }) !isInternal && - it.only("can update a row on an external table with a primary key", async () => { + it("can update a row on an external table with a primary key", async () => { const tableName = uuid.v4().substring(0, 10) await client!.schema.createTable(tableName, table => { table.increments("id").primary() diff --git a/packages/server/src/integrations/base/types.ts b/packages/server/src/integrations/base/types.ts index 463d73444b..1d0dee97fa 100644 --- a/packages/server/src/integrations/base/types.ts +++ b/packages/server/src/integrations/base/types.ts @@ -104,11 +104,34 @@ export interface OracleColumnsResponse { SEARCH_CONDITION: null | string } +export enum TriggeringEvent { + INSERT = "INSERT", + DELETE = "DELETE", + UPDATE = "UPDATE", + LOGON = "LOGON", + LOGOFF = "LOGOFF", + STARTUP = "STARTUP", + SHUTDOWN = "SHUTDOWN", + SERVERERROR = "SERVERERROR", + SCHEMA = "SCHEMA", + ALTER = "ALTER", + DROP = "DROP", +} + +export enum TriggerType { + BEFORE_EACH_ROW = "BEFORE EACH ROW", + AFTER_EACH_ROW = "AFTER EACH ROW", + BEFORE_STATEMENT = "BEFORE STATEMENT", + AFTER_STATEMENT = "AFTER STATEMENT", + INSTEAD_OF = "INSTEAD OF", + COMPOUND = "COMPOUND", +} + export interface OracleTriggersResponse { TABLE_NAME: string TRIGGER_NAME: string - TRIGGER_TYPE: string - TRIGGERING_EVENT: string + TRIGGER_TYPE: TriggerType + TRIGGERING_EVENT: TriggeringEvent TRIGGER_BODY: string } diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts index 956526e8cf..d1c0978b89 100644 --- a/packages/server/src/integrations/oracle.ts +++ b/packages/server/src/integrations/oracle.ts @@ -36,6 +36,8 @@ import { OracleColumn, OracleColumnsResponse, OracleTriggersResponse, + TriggeringEvent, + TriggerType, } from "./base/types" import { sql } from "@budibase/backend-core" @@ -146,7 +148,15 @@ class OracleIntegration extends Sql implements DatasourcePlus { ` private static readonly TRIGGERS_SQL = ` - SELECT table_name, trigger_name, trigger_type, triggering_event, trigger_body FROM all_triggers WHERE status = 'ENABLED'; + SELECT + table_name, + trigger_name, + trigger_type, + triggering_event, + trigger_body + FROM + all_triggers + WHERE status = 'ENABLED' ` constructor(config: OracleConfig) { @@ -221,6 +231,75 @@ class OracleIntegration extends Sql implements DatasourcePlus { return oracleTables } + private getTriggersFor( + tableName: string, + triggersResponse: Result, + opts?: { event?: TriggeringEvent; type?: TriggerType } + ): OracleTriggersResponse[] { + const triggers: OracleTriggersResponse[] = [] + for (const trigger of triggersResponse.rows || []) { + if (trigger.TABLE_NAME !== tableName) { + continue + } + if (opts?.event && opts.event !== trigger.TRIGGERING_EVENT) { + continue + } + if (opts?.type && opts.type !== trigger.TRIGGER_TYPE) { + continue + } + triggers.push(trigger) + } + return triggers + } + + private markAutoIncrementColumns( + triggersResponse: Result, + tables: Record + ) { + for (const table of Object.values(tables)) { + const triggers = this.getTriggersFor(table.name, triggersResponse, { + type: TriggerType.BEFORE_EACH_ROW, + event: TriggeringEvent.INSERT, + }) + + // This is the trigger body Knex generates for an auto increment column + // called "id" on a table called "foo": + // + // declare checking number := 1; + // begin if (:new. "id" is null) then while checking >= 1 loop + // select + // "foo_seq".nextval into :new. "id" + // from + // dual; + // select + // count("id") into checking + // from + // "foo" + // where + // "id" = :new. "id"; + // end loop; + // end if; + // end; + for (const [columnName, schema] of Object.entries(table.schema)) { + const autoIncrementTriggers = triggers.filter( + trigger => + // This is a bit heuristic, but I think it's the best we can do with + // the information we have. We're looking for triggers that run + // before each row is inserted, and that have a body that contains a + // call to a function that generates a new value for the column. We + // also check that the column name is in the trigger body, to make + // sure we're not picking up triggers that don't affect the column. + trigger.TRIGGER_BODY.includes(`"${columnName}"`) && + trigger.TRIGGER_BODY.includes(`.nextval`) + ) + + if (autoIncrementTriggers.length > 0) { + schema.autocolumn = true + } + } + } + } + private static isSupportedColumn(column: OracleColumn) { return !UNSUPPORTED_TYPES.includes(column.type) } @@ -331,6 +410,8 @@ class OracleIntegration extends Sql implements DatasourcePlus { }) }) + this.markAutoIncrementColumns(triggersResponse, tables) + let externalTables = finaliseExternalTables(tables, entities) let errors = checkExternalTables(externalTables) return { tables: externalTables, errors }