diff --git a/agents/drivers/oracle-go/main.go b/agents/drivers/oracle-go/main.go
index d287429a06..a4865e1c9c 100644
--- a/agents/drivers/oracle-go/main.go
+++ b/agents/drivers/oracle-go/main.go
@@ -235,6 +235,9 @@ SELECT t.TRIGGER_NAME,
t.TRIGGERING_EVENT,
t.TRIGGER_TYPE,
t.DESCRIPTION,
+ t.STATUS,
+ t.WHEN_CLAUSE,
+ t.REFERENCING_NAMES,
s.LINE,
s.TEXT
FROM ALL_TRIGGERS t
@@ -523,6 +526,7 @@ type triggerInfo struct {
Event string `json:"event"`
Timing string `json:"timing"`
Statement *string `json:"statement,omitempty"`
+ Enabled *bool `json:"enabled,omitempty"`
}
// constraintInfo represents primary key, unique, and check constraints for a
@@ -2977,28 +2981,38 @@ func (s *server) listTriggers(schema, table string) ([]triggerInfo, error) {
var result []triggerInfo
var currentName string
var currentDescription string
+ var currentStructured bool
var source strings.Builder
flush := func() {
if len(result) == 0 || currentName == "" {
return
}
+ if !currentStructured {
+ // Keep the declaration so callers can tell the source carries clauses the
+ // timing/event/body form cannot round-trip.
+ if body := strings.TrimSpace(strings.ReplaceAll(source.String(), "\r\n", "\n")); body != "" {
+ result[len(result)-1].Statement = &body
+ }
+ return
+ }
if body, ok := oracleTriggerBody(source.String(), currentDescription); ok {
result[len(result)-1].Statement = &body
}
}
for rows.Next() {
var name, event, timing string
- var description, lineText sql.NullString
+ var description, status, whenClause, referencingNames, lineText sql.NullString
var line sql.NullInt64
- if err := rows.Scan(&name, &event, &timing, &description, &line, &lineText); err != nil {
+ if err := rows.Scan(&name, &event, &timing, &description, &status, &whenClause, &referencingNames, &line, &lineText); err != nil {
return nil, err
}
if name != currentName {
flush()
currentName = name
currentDescription = description.String
+ currentStructured = oracleTriggerIsStructured(description.String, whenClause.String, referencingNames.String)
source.Reset()
- result = append(result, triggerInfo{Name: name, Event: event, Timing: timing})
+ result = append(result, triggerInfo{Name: name, Event: event, Timing: timing, Enabled: oracleTriggerEnabled(status)})
}
if line.Valid && lineText.Valid {
source.WriteString(lineText.String)
@@ -3008,6 +3022,33 @@ func (s *server) listTriggers(schema, table string) ([]triggerInfo, error) {
return emptyIfNil(result), rows.Err()
}
+var oracleTriggerUnstructuredClauseRegexp = regexp.MustCompile(`(?i)\b(?:FOLLOWS|PRECEDES|COMPOUND|CROSSEDITION)\b`)
+
+// oracleTriggerIsStructured reports whether a trigger is fully described by its timing, event and
+// PL/SQL body, so it can be rebuilt with CREATE OR REPLACE without dropping a WHEN condition,
+// custom REFERENCING names or FOLLOWS/PRECEDES ordering.
+func oracleTriggerIsStructured(description, whenClause, referencingNames string) bool {
+ if strings.TrimSpace(whenClause) != "" {
+ return false
+ }
+ if referencing := strings.Join(strings.Fields(strings.ToUpper(referencingNames)), " "); referencing != "" && referencing != "REFERENCING NEW AS NEW OLD AS OLD" {
+ return false
+ }
+ return !oracleTriggerUnstructuredClauseRegexp.MatchString(description)
+}
+
+func oracleTriggerEnabled(status sql.NullString) *bool {
+ switch strings.ToUpper(strings.TrimSpace(status.String)) {
+ case "ENABLED":
+ enabled := true
+ return &enabled
+ case "DISABLED":
+ enabled := false
+ return &enabled
+ }
+ return nil
+}
+
func oracleTriggerBody(source, description string) (string, bool) {
source = strings.ReplaceAll(source, "\r\n", "\n")
description = strings.ReplaceAll(description, "\r\n", "\n")
diff --git a/agents/drivers/oracle-go/main_test.go b/agents/drivers/oracle-go/main_test.go
index 49e25321e9..354cb17be8 100644
--- a/agents/drivers/oracle-go/main_test.go
+++ b/agents/drivers/oracle-go/main_test.go
@@ -1109,8 +1109,8 @@ func TestListForeignKeysAndTriggersPreserveQuotedCloneTableName(t *testing.T) {
{
queryContains: "FROM ALL_TRIGGERS",
args: []driver.Value{schema, table},
- columns: []string{"TRIGGER_NAME", "TRIGGERING_EVENT", "TRIGGER_TYPE", "DESCRIPTION", "LINE", "TEXT"},
- rows: [][]driver.Value{{"ORDERS_copy_TRG1", "INSERT", "BEFORE EACH ROW", nil, nil, nil}},
+ columns: []string{"TRIGGER_NAME", "TRIGGERING_EVENT", "TRIGGER_TYPE", "DESCRIPTION", "STATUS", "WHEN_CLAUSE", "REFERENCING_NAMES", "LINE", "TEXT"},
+ rows: [][]driver.Value{{"ORDERS_copy_TRG1", "INSERT", "BEFORE EACH ROW", nil, "DISABLED", nil, nil, nil, nil}},
},
})
s := newServer()
@@ -1130,6 +1130,9 @@ func TestListForeignKeysAndTriggersPreserveQuotedCloneTableName(t *testing.T) {
if len(triggers) != 1 || triggers[0].Name != "ORDERS_copy_TRG1" {
t.Fatalf("listTriggers(%q) = %#v, want quoted clone trigger", table, triggers)
}
+ if triggers[0].Enabled == nil || *triggers[0].Enabled {
+ t.Fatalf("listTriggers(%q) enabled = %v, want disabled from ALL_TRIGGERS.STATUS", table, triggers[0].Enabled)
+ }
if scripted.next != len(scripted.steps) {
t.Fatalf("expected %d queries, got %d", len(scripted.steps), scripted.next)
}
@@ -2216,6 +2219,11 @@ func TestOracleListTriggersSQLLoadsSourceWithoutLongColumns(t *testing.T) {
if !strings.Contains(sqlText, "T.DESCRIPTION") || !strings.Contains(sqlText, "S.TEXT") {
t.Fatalf("trigger listing should load the declaration and source text, got: %s", oracleListTriggersSQL)
}
+ for _, column := range []string{"T.STATUS", "T.WHEN_CLAUSE", "T.REFERENCING_NAMES"} {
+ if !strings.Contains(sqlText, column) {
+ t.Fatalf("trigger listing should load %s, got: %s", column, oracleListTriggersSQL)
+ }
+ }
if strings.Contains(sqlText, "TRIGGER_BODY") {
t.Fatalf("trigger listing should avoid Oracle LONG trigger bodies, got: %s", oracleListTriggersSQL)
}
@@ -2247,6 +2255,69 @@ func TestOracleTriggerBodyStripsDictionaryDeclaration(t *testing.T) {
}
}
+func TestListTriggersKeepsDeclarationForUnstructuredTriggers(t *testing.T) {
+ const schema = "HR"
+ const table = "ORDERS"
+ columns := []string{"TRIGGER_NAME", "TRIGGERING_EVENT", "TRIGGER_TYPE", "DESCRIPTION", "STATUS", "WHEN_CLAUSE", "REFERENCING_NAMES", "LINE", "TEXT"}
+ db, _ := openOracleViewSourceTestDB(t, []oracleViewSourceQueryStep{
+ {
+ queryContains: "FROM ALL_TRIGGERS",
+ args: []driver.Value{schema, table},
+ columns: columns,
+ rows: [][]driver.Value{
+ {"PLAIN_TRG", "INSERT", "BEFORE EACH ROW", "PLAIN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", nil, "REFERENCING NEW AS NEW OLD AS OLD", int64(1), "TRIGGER PLAIN_TRG\n"},
+ {"PLAIN_TRG", "INSERT", "BEFORE EACH ROW", "PLAIN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", nil, "REFERENCING NEW AS NEW OLD AS OLD", int64(2), "BEFORE INSERT ON ORDERS\n"},
+ {"PLAIN_TRG", "INSERT", "BEFORE EACH ROW", "PLAIN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", nil, "REFERENCING NEW AS NEW OLD AS OLD", int64(3), "FOR EACH ROW\n"},
+ {"PLAIN_TRG", "INSERT", "BEFORE EACH ROW", "PLAIN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", nil, "REFERENCING NEW AS NEW OLD AS OLD", int64(4), "BEGIN NULL; END;\n"},
+ {"WHEN_TRG", "INSERT", "BEFORE EACH ROW", "WHEN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", "NEW.STATUS = 'OPEN'", "REFERENCING NEW AS NEW OLD AS OLD", int64(1), "TRIGGER WHEN_TRG\n"},
+ {"WHEN_TRG", "INSERT", "BEFORE EACH ROW", "WHEN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", "NEW.STATUS = 'OPEN'", "REFERENCING NEW AS NEW OLD AS OLD", int64(2), "BEFORE INSERT ON ORDERS\n"},
+ {"WHEN_TRG", "INSERT", "BEFORE EACH ROW", "WHEN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", "NEW.STATUS = 'OPEN'", "REFERENCING NEW AS NEW OLD AS OLD", int64(3), "FOR EACH ROW\n"},
+ {"WHEN_TRG", "INSERT", "BEFORE EACH ROW", "WHEN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", "NEW.STATUS = 'OPEN'", "REFERENCING NEW AS NEW OLD AS OLD", int64(4), "WHEN (NEW.STATUS = 'OPEN')\n"},
+ {"WHEN_TRG", "INSERT", "BEFORE EACH ROW", "WHEN_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n", "ENABLED", "NEW.STATUS = 'OPEN'", "REFERENCING NEW AS NEW OLD AS OLD", int64(5), "BEGIN NULL; END;\n"},
+ },
+ },
+ })
+ s := newServer()
+ s.db = db
+
+ triggers, err := s.listTriggers(schema, table)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(triggers) != 2 || triggers[0].Statement == nil || triggers[1].Statement == nil {
+ t.Fatalf("listTriggers = %#v, want two triggers with source", triggers)
+ }
+ if got := *triggers[0].Statement; got != "BEGIN NULL; END;" {
+ t.Fatalf("plain trigger statement = %q, want stripped body", got)
+ }
+ if got := *triggers[1].Statement; !strings.HasPrefix(got, "TRIGGER WHEN_TRG\n") || !strings.Contains(got, "WHEN (NEW.STATUS = 'OPEN')") {
+ t.Fatalf("WHEN trigger statement = %q, want full declaration kept", got)
+ }
+ if triggers[0].Enabled == nil || !*triggers[0].Enabled {
+ t.Fatalf("plain trigger enabled = %v, want true", triggers[0].Enabled)
+ }
+}
+
+func TestOracleTriggerIsStructured(t *testing.T) {
+ const description = "AUDIT_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\n"
+ const defaultReferencing = "REFERENCING NEW AS NEW OLD AS OLD"
+ if !oracleTriggerIsStructured(description, "", defaultReferencing) {
+ t.Fatal("plain row trigger should be structured")
+ }
+ if !oracleTriggerIsStructured("AUDIT_TRG\nAFTER INSERT ON ORDERS\n", "", "") {
+ t.Fatal("statement trigger without referencing names should be structured")
+ }
+ if oracleTriggerIsStructured(description, "NEW.ID > 0", defaultReferencing) {
+ t.Fatal("WHEN condition should not be structured")
+ }
+ if oracleTriggerIsStructured(description, "", "REFERENCING NEW AS N OLD AS O") {
+ t.Fatal("custom REFERENCING names should not be structured")
+ }
+ if oracleTriggerIsStructured("AUDIT_TRG\nBEFORE INSERT ON ORDERS\nFOR EACH ROW\nFOLLOWS OTHER_TRG\n", "", defaultReferencing) {
+ t.Fatal("FOLLOWS ordering should not be structured")
+ }
+}
+
func TestOracleTriggerBodyFallsBackToVisibleSource(t *testing.T) {
body, ok := oracleTriggerBody("TRIGGER APP.AUDIT\nBEGIN\n NULL;\nEND;\n", "differently formatted declaration")
if !ok {
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index 2ed176ea94..ea96082896 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -1012,7 +1012,7 @@ function foreignKeyChanged(foreignKey: EditableStructureForeignKey): boolean {
function triggerChanged(trigger: EditableStructureTrigger): boolean {
if (!trigger.original || trigger.markedForDrop) return true;
const original = trigger.original;
- return trigger.name !== original.name || trigger.timing !== original.timing || trigger.event !== original.event || !sameText(trigger.statement, original.statement);
+ return trigger.name !== original.name || trigger.timing !== original.timing || trigger.event !== original.event || !sameText(trigger.statement, original.statement) || (trigger.enabled !== undefined && trigger.enabled !== original.enabled);
}
function captureStructureRefreshScope(): TableStructureRefreshScope {
@@ -5150,6 +5150,7 @@ function addTrigger() {
timing: isOracleTriggerEditor.value ? "BEFORE EACH ROW" : isSqlServerTriggerEditor.value ? "AFTER" : "BEFORE",
event: "INSERT",
statement: isOracleTriggerEditor.value ? "BEGIN\n NULL;\nEND" : isSqlServerTriggerEditor.value ? "BEGIN\n SET NOCOUNT ON;\nEND" : "BEGIN\n \nEND",
+ enabled: true,
markedForDrop: false,
};
triggers.value.push(draft);
@@ -5166,7 +5167,13 @@ function toggleDropTrigger(trigger: EditableStructureTrigger) {
}
function canEditTriggerDraft(trigger: EditableStructureTrigger): boolean {
- return !triggersLoading.value && canEditTriggers.value && !trigger.markedForDrop && canEditStructuredTriggerDraft(databaseType.value, trigger);
+ return canToggleTriggerEnabled(trigger) && canEditStructuredTriggerDraft(databaseType.value, trigger);
+}
+
+// ALTER TRIGGER ... ENABLE/DISABLE leaves the source untouched, so it stays available for
+// triggers whose source the structured form cannot rebuild.
+function canToggleTriggerEnabled(trigger: EditableStructureTrigger): boolean {
+ return !triggersLoading.value && canEditTriggers.value && !trigger.markedForDrop;
}
function primarySqlOperation(sql: string): string {
@@ -7016,8 +7023,18 @@ watch(
{{ event }}
-
- {{ trigger.original.enabled ? t("damengJobAdmin.enabled") : t("damengJobAdmin.disabled") }}
+
+
+ {{ trigger.enabled ? t("damengJobAdmin.enabled") : t("damengJobAdmin.disabled") }}