diff --git a/pg/catalog/alter.go b/pg/catalog/alter.go index 6ff55612..96d89575 100644 --- a/pg/catalog/alter.go +++ b/pg/catalog/alter.go @@ -676,6 +676,8 @@ func (c *Catalog) ExecRenameStmt(stmt *nodes.RenameStmt) error { return c.renameSequence(stmt) case nodes.OBJECT_TRIGGER: return c.renameTrigger(stmt) + case nodes.OBJECT_EVENT_TRIGGER: + return c.renameEventTrigger(stmt) case nodes.OBJECT_TABCONSTRAINT: return c.renameConstraint(stmt) case nodes.OBJECT_FUNCTION, nodes.OBJECT_PROCEDURE, nodes.OBJECT_ROUTINE: diff --git a/pg/catalog/catalog.go b/pg/catalog/catalog.go index f87fbdeb..5768392f 100644 --- a/pg/catalog/catalog.go +++ b/pg/catalog/catalog.go @@ -50,11 +50,11 @@ type Catalog struct { relationByOID map[uint32]*Relation // Constraint indexes. - constraints map[uint32]*Constraint // OID → Constraint + constraints map[uint32]*Constraint // OID → Constraint consByRel map[uint32][]*Constraint // relOID → constraints // Index indexes. - indexes map[uint32]*Index // OID → Index + indexes map[uint32]*Index // OID → Index indexesByRel map[uint32][]*Index // relOID → indexes // Sequence index. @@ -76,6 +76,10 @@ type Catalog struct { triggers map[uint32]*Trigger triggersByRel map[uint32][]*Trigger + // Event triggers. + eventTriggers map[uint32]*EventTrigger + eventTriggerByName map[string]*EventTrigger + // Comments. comments map[commentKey]string @@ -130,31 +134,33 @@ type Catalog struct { // New creates a fully initialized Catalog with all built-in data indexed. func New() *Catalog { c := &Catalog{ - oidGen: NewOIDGenerator(), - schemas: make(map[uint32]*Schema), - schemaByName: make(map[string]*Schema), - typeByOID: make(map[uint32]*BuiltinType, len(BuiltinTypes)), - typeByName: make(map[typeKey]*BuiltinType, len(BuiltinTypes)), - castIndex: make(map[castKey]*BuiltinCast, len(BuiltinCasts)), - operByOID: make(map[uint32]*BuiltinOperator, len(BuiltinOperators)), - operByKey: make(map[operKey][]*BuiltinOperator, len(BuiltinOperators)), - procByOID: make(map[uint32]*BuiltinProc, len(BuiltinProcs)), - procByName: make(map[string][]*BuiltinProc), - relationByOID: make(map[uint32]*Relation), - constraints: make(map[uint32]*Constraint), - consByRel: make(map[uint32][]*Constraint), - indexes: make(map[uint32]*Index), - indexesByRel: make(map[uint32][]*Index), - sequenceByOID: make(map[uint32]*Sequence), - enumTypes: make(map[uint32]*EnumType), - domainTypes: make(map[uint32]*DomainType), - rangeTypes: make(map[uint32]*RangeType), - userProcs: make(map[uint32]*UserProc), - triggers: make(map[uint32]*Trigger), - triggersByRel: make(map[uint32][]*Trigger), - comments: make(map[commentKey]string), - policies: make(map[uint32]*Policy), - policiesByRel: make(map[uint32][]*Policy), + oidGen: NewOIDGenerator(), + schemas: make(map[uint32]*Schema), + schemaByName: make(map[string]*Schema), + typeByOID: make(map[uint32]*BuiltinType, len(BuiltinTypes)), + typeByName: make(map[typeKey]*BuiltinType, len(BuiltinTypes)), + castIndex: make(map[castKey]*BuiltinCast, len(BuiltinCasts)), + operByOID: make(map[uint32]*BuiltinOperator, len(BuiltinOperators)), + operByKey: make(map[operKey][]*BuiltinOperator, len(BuiltinOperators)), + procByOID: make(map[uint32]*BuiltinProc, len(BuiltinProcs)), + procByName: make(map[string][]*BuiltinProc), + relationByOID: make(map[uint32]*Relation), + constraints: make(map[uint32]*Constraint), + consByRel: make(map[uint32][]*Constraint), + indexes: make(map[uint32]*Index), + indexesByRel: make(map[uint32][]*Index), + sequenceByOID: make(map[uint32]*Sequence), + enumTypes: make(map[uint32]*EnumType), + domainTypes: make(map[uint32]*DomainType), + rangeTypes: make(map[uint32]*RangeType), + userProcs: make(map[uint32]*UserProc), + triggers: make(map[uint32]*Trigger), + triggersByRel: make(map[uint32][]*Trigger), + eventTriggers: make(map[uint32]*EventTrigger), + eventTriggerByName: make(map[string]*EventTrigger), + comments: make(map[commentKey]string), + policies: make(map[uint32]*Policy), + policiesByRel: make(map[uint32][]*Policy), extensions: make(map[uint32]*Extension), extByName: make(map[string]*Extension), accessMethods: make(map[uint32]*AccessMethod), diff --git a/pg/catalog/clone.go b/pg/catalog/clone.go index 6ae24ec0..b84ad470 100644 --- a/pg/catalog/clone.go +++ b/pg/catalog/clone.go @@ -304,6 +304,19 @@ func (c *Catalog) Clone() *Catalog { clone.triggersByRel[ct.RelOID] = append(clone.triggersByRel[ct.RelOID], &ct) } + // --- Event triggers: deep copy + rebuild name index --- + clone.eventTriggers = make(map[uint32]*EventTrigger, len(c.eventTriggers)) + clone.eventTriggerByName = make(map[string]*EventTrigger, len(c.eventTriggerByName)) + for oid, evt := range c.eventTriggers { + ce := *evt + if evt.Tags != nil { + ce.Tags = make([]string, len(evt.Tags)) + copy(ce.Tags, evt.Tags) + } + clone.eventTriggers[oid] = &ce + clone.eventTriggerByName[ce.Name] = &ce + } + // --- Comments: copy map --- clone.comments = make(map[commentKey]string, len(c.comments)) for k, v := range c.comments { diff --git a/pg/catalog/comment.go b/pg/catalog/comment.go index 8441f074..8a54e5a3 100644 --- a/pg/catalog/comment.go +++ b/pg/catalog/comment.go @@ -282,6 +282,14 @@ func (c *Catalog) CommentObject(stmt *nodes.CommentStmt) error { return &Error{Code: CodeUndefinedObject, Message: fmt.Sprintf("trigger %q does not exist", trigName)} } + case nodes.OBJECT_EVENT_TRIGGER: + name := extractSimpleObjectName(stmt.Object) + evt := c.eventTriggerByName[name] + if evt == nil { + return errUndefinedObject("event trigger", name) + } + key = commentKey{ObjType: 'E', ObjOID: evt.OID} + case nodes.OBJECT_COLLATION, nodes.OBJECT_CONVERSION, nodes.OBJECT_OPERATOR, @@ -301,7 +309,6 @@ func (c *Catalog) CommentObject(stmt *nodes.CommentStmt) error { nodes.OBJECT_EXTENSION, nodes.OBJECT_ACCESS_METHOD, nodes.OBJECT_PUBLICATION, - nodes.OBJECT_EVENT_TRIGGER, nodes.OBJECT_DATABASE, nodes.OBJECT_TABLESPACE, nodes.OBJECT_ROLE, @@ -361,4 +368,3 @@ func (c *Catalog) removeComments(objType byte, objOID uint32) { } } } - diff --git a/pg/catalog/constraint.go b/pg/catalog/constraint.go index e3e1b763..85137042 100644 --- a/pg/catalog/constraint.go +++ b/pg/catalog/constraint.go @@ -879,6 +879,12 @@ func (c *Catalog) dropDependents(refType byte, refOID uint32) { continue } c.removeTrigger(trig) + case 'E': + evt := c.eventTriggers[dep.ObjOID] + if evt == nil { + continue + } + c.removeEventTrigger(evt) } } } diff --git a/pg/catalog/diff.go b/pg/catalog/diff.go index 70c186b9..a384949d 100644 --- a/pg/catalog/diff.go +++ b/pg/catalog/diff.go @@ -26,18 +26,18 @@ type SchemaDiffEntry struct { // RelationDiffEntry describes a relation (table/view/matview) change. type RelationDiffEntry struct { - Action DiffAction - SchemaName string - Name string - From *Relation - To *Relation - Columns []ColumnDiffEntry - Constraints []ConstraintDiffEntry - Indexes []IndexDiffEntry - Triggers []TriggerDiffEntry - Policies []PolicyDiffEntry - RLSChanged bool - RLSEnabled bool + Action DiffAction + SchemaName string + Name string + From *Relation + To *Relation + Columns []ColumnDiffEntry + Constraints []ConstraintDiffEntry + Indexes []IndexDiffEntry + Triggers []TriggerDiffEntry + Policies []PolicyDiffEntry + RLSChanged bool + RLSEnabled bool ForceRLSEnabled bool } @@ -92,6 +92,14 @@ type TriggerDiffEntry struct { To *Trigger } +// EventTriggerDiffEntry describes a database-level event trigger change. +type EventTriggerDiffEntry struct { + Action DiffAction + Name string + From *EventTrigger + To *EventTrigger +} + // EnumDiffEntry describes an enum type change. type EnumDiffEntry struct { Action DiffAction @@ -176,6 +184,7 @@ type SchemaDiff struct { Ranges []RangeDiffEntry CompositeTypes []CompositeTypeDiffEntry Extensions []ExtensionDiffEntry + EventTriggers []EventTriggerDiffEntry Comments []CommentDiffEntry Grants []GrantDiffEntry } @@ -191,6 +200,7 @@ func (d *SchemaDiff) IsEmpty() bool { len(d.Ranges) == 0 && len(d.CompositeTypes) == 0 && len(d.Extensions) == 0 && + len(d.EventTriggers) == 0 && len(d.Comments) == 0 && len(d.Grants) == 0 } @@ -210,6 +220,7 @@ func Diff(from, to *Catalog) *SchemaDiff { Ranges: diffRanges(from, to), CompositeTypes: diffComposites(from, to), Extensions: diffExtensions(from, to), + EventTriggers: diffEventTriggers(from, to), Comments: diffComments(from, to), Grants: diffGrants(from, to), } diff --git a/pg/catalog/diff_comment.go b/pg/catalog/diff_comment.go index 3b4a5193..dfa3d97e 100644 --- a/pg/catalog/diff_comment.go +++ b/pg/catalog/diff_comment.go @@ -187,6 +187,13 @@ func resolveCommentDescription(c *Catalog, ck commentKey) string { } return fmt.Sprintf("%s.%s.%s", rel.Schema.Name, rel.Name, trig.Name) + case 'E': // event trigger + evt := c.eventTriggers[ck.ObjOID] + if evt == nil { + return "" + } + return evt.Name + case 'd': // domain constraint // Domain constraints are stored on DomainType.Constraints. for _, dt := range c.domainTypes { @@ -222,4 +229,3 @@ func resolveCommentDescription(c *Catalog, ck commentKey) string { return "" } } - diff --git a/pg/catalog/dropcmds.go b/pg/catalog/dropcmds.go index 007b2444..ab4a5c20 100644 --- a/pg/catalog/dropcmds.go +++ b/pg/catalog/dropcmds.go @@ -31,6 +31,8 @@ func (c *Catalog) RemoveObjects(stmt *nodes.DropStmt) error { return c.removeFunctionObjects(stmt) case nodes.OBJECT_TRIGGER: return c.removeTriggerObjects(stmt) + case nodes.OBJECT_EVENT_TRIGGER: + return c.removeEventTriggerObjects(stmt) case nodes.OBJECT_POLICY: return c.removePolicyObjects(stmt) case nodes.OBJECT_TABLE, nodes.OBJECT_VIEW, nodes.OBJECT_MATVIEW: @@ -53,7 +55,6 @@ func (c *Catalog) RemoveObjects(stmt *nodes.DropStmt) error { nodes.OBJECT_FOREIGN_SERVER, nodes.OBJECT_ACCESS_METHOD, nodes.OBJECT_PUBLICATION, - nodes.OBJECT_EVENT_TRIGGER, nodes.OBJECT_RULE, nodes.OBJECT_CAST, nodes.OBJECT_TRANSFORM, diff --git a/pg/catalog/event_trigger.go b/pg/catalog/event_trigger.go new file mode 100644 index 00000000..a25a6db7 --- /dev/null +++ b/pg/catalog/event_trigger.go @@ -0,0 +1,503 @@ +package catalog + +import ( + "fmt" + "sort" + "strings" + + nodes "github.com/bytebase/omni/pg/ast" +) + +// EventTrigger represents a PostgreSQL event trigger in pg_event_trigger. +type EventTrigger struct { + OID uint32 + Name string + EventName string + FuncOID uint32 + Enabled byte + Tags []string +} + +// CreateEventTriggerStmt creates a database-level event trigger. +// +// pg: src/backend/commands/event_trigger.c — CreateEventTrigger +func (c *Catalog) CreateEventTriggerStmt(stmt *nodes.CreateEventTrigStmt) error { + if !validEventTriggerEvent(stmt.Eventname) { + return &Error{Code: CodeSyntaxError, Message: fmt.Sprintf("unrecognized event name %q", stmt.Eventname)} + } + tags, err := eventTriggerTags(stmt.Eventname, stmt.Whenclause) + if err != nil { + return err + } + if c.eventTriggerByName[stmt.Trigname] != nil { + return errDuplicateObject("event trigger", stmt.Trigname) + } + + funcSchema, funcName := qualifiedName(stmt.Funcname) + funcBP, wrongRet := c.findEventTriggerFunc(funcSchema, funcName) + if funcBP == nil { + if wrongRet { + return errInvalidObjectDefinition(fmt.Sprintf("function %s must return type event_trigger", funcName)) + } + return errUndefinedFunction(funcName, nil) + } + + oid := c.oidGen.Next() + evt := &EventTrigger{ + OID: oid, + Name: stmt.Trigname, + EventName: stmt.Eventname, + FuncOID: funcBP.OID, + Enabled: 'O', + Tags: tags, + } + c.eventTriggers[oid] = evt + c.eventTriggerByName[evt.Name] = evt + + // pg: event_trigger.c — event trigger depends on its function. + c.recordDependency('E', oid, 0, 'f', funcBP.OID, 0, DepNormal) + return nil +} + +// AlterEventTrigger updates an event trigger's firing configuration. +// +// pg: src/backend/commands/event_trigger.c — AlterEventTrigger +func (c *Catalog) AlterEventTrigger(stmt *nodes.AlterEventTrigStmt) error { + evt := c.eventTriggerByName[stmt.Trigname] + if evt == nil { + return errUndefinedObject("event trigger", stmt.Trigname) + } + evt.Enabled = stmt.Tgenabled + return nil +} + +func (c *Catalog) renameEventTrigger(stmt *nodes.RenameStmt) error { + oldName := extractSimpleObjectName(stmt.Object) + if oldName == "" { + oldName = stmt.Subname + } + evt := c.eventTriggerByName[oldName] + if evt == nil { + return errUndefinedObject("event trigger", oldName) + } + if c.eventTriggerByName[stmt.Newname] != nil { + return errDuplicateObject("event trigger", stmt.Newname) + } + delete(c.eventTriggerByName, oldName) + evt.Name = stmt.Newname + c.eventTriggerByName[evt.Name] = evt + return nil +} + +func (c *Catalog) removeEventTriggerObjects(stmt *nodes.DropStmt) error { + if stmt.Objects == nil { + return nil + } + for _, obj := range stmt.Objects.Items { + _, name := extractDropObjectName(obj) + evt := c.eventTriggerByName[name] + if evt == nil { + if stmt.Missing_ok { + c.addWarning(CodeWarningSkip, fmt.Sprintf("event trigger %q does not exist, skipping", name)) + continue + } + return errUndefinedObject("event trigger", name) + } + c.removeEventTrigger(evt) + } + return nil +} + +func (c *Catalog) removeEventTrigger(evt *EventTrigger) { + delete(c.eventTriggers, evt.OID) + delete(c.eventTriggerByName, evt.Name) + c.removeComments('E', evt.OID) + c.removeDepsOf('E', evt.OID) + c.removeDepsOn('E', evt.OID) +} + +func validEventTriggerEvent(event string) bool { + switch event { + case "ddl_command_start", "ddl_command_end", "sql_drop", "login", "table_rewrite": + return true + default: + return false + } +} + +func eventTriggerTags(event string, when *nodes.List) ([]string, error) { + var tags []string + var items []nodes.Node + if when != nil { + items = when.Items + } + for _, item := range items { + def, ok := item.(*nodes.DefElem) + if !ok { + continue + } + if def.Defname != "tag" { + return nil, &Error{Code: CodeSyntaxError, Message: fmt.Sprintf("unrecognized filter variable %q", def.Defname)} + } + if tags != nil { + return nil, &Error{Code: CodeSyntaxError, Message: fmt.Sprintf("filter variable %q specified more than once", def.Defname)} + } + l, ok := def.Arg.(*nodes.List) + if !ok { + continue + } + for _, tagItem := range l.Items { + tag := strings.ToUpper(stringVal(tagItem)) + if tag == "" { + continue + } + tags = append(tags, tag) + } + } + + if len(tags) == 0 { + return nil, nil + } + switch event { + case "login": + return nil, &Error{Code: CodeFeatureNotSupported, Message: "tag filtering is not supported for login event triggers"} + case "table_rewrite": + for _, tag := range tags { + if !eventTriggerTableRewriteTagOK(tag) { + return nil, &Error{Code: CodeFeatureNotSupported, Message: fmt.Sprintf("event triggers are not supported for %s", tag)} + } + } + default: + for _, tag := range tags { + if !eventTriggerKnownDDLTags[tag] && !eventTriggerUnsupportedDDLTags[tag] { + return nil, &Error{Code: CodeSyntaxError, Message: fmt.Sprintf("filter value %q not recognized for filter variable %q", tag, "tag")} + } + if !eventTriggerDDLTagOK(tag) { + return nil, &Error{Code: CodeFeatureNotSupported, Message: fmt.Sprintf("event triggers are not supported for %s", tag)} + } + } + } + return tags, nil +} + +func eventTriggerDDLTagOK(tag string) bool { + if tag == "" { + return false + } + if eventTriggerUnsupportedDDLTags[tag] { + return false + } + return eventTriggerKnownDDLTags[tag] +} + +func eventTriggerTableRewriteTagOK(tag string) bool { + return eventTriggerTableRewriteTags[tag] +} + +var eventTriggerUnsupportedDDLTags = map[string]bool{ + "ALTER DATABASE": true, + "ALTER EVENT TRIGGER": true, + "ALTER ROLE": true, + "ALTER SYSTEM": true, + "ALTER TABLESPACE": true, + "ANALYZE": true, + "BEGIN": true, + "CALL": true, + "CHECKPOINT": true, + "CLOSE": true, + "CLOSE CURSOR": true, + "CLOSE CURSOR ALL": true, + "CLUSTER": true, + "COMMIT": true, + "COMMIT PREPARED": true, + "COPY": true, + "COPY FROM": true, + "CREATE DATABASE": true, + "CREATE EVENT TRIGGER": true, + "CREATE ROLE": true, + "CREATE TABLESPACE": true, + "DEALLOCATE": true, + "DEALLOCATE ALL": true, + "DECLARE CURSOR": true, + "DELETE": true, + "DISCARD": true, + "DISCARD ALL": true, + "DISCARD PLANS": true, + "DISCARD SEQUENCES": true, + "DISCARD TEMP": true, + "DO": true, + "DROP DATABASE": true, + "DROP EVENT TRIGGER": true, + "DROP ROLE": true, + "DROP TABLESPACE": true, + "EXECUTE": true, + "EXPLAIN": true, + "FETCH": true, + "GRANT ROLE": true, + "INSERT": true, + "LISTEN": true, + "LOAD": true, + "LOCK TABLE": true, + "MERGE": true, + "MOVE": true, + "NOTIFY": true, + "PREPARE": true, + "PREPARE TRANSACTION": true, + "REASSIGN OWNED": true, + "RELEASE": true, + "RESET": true, + "REVOKE ROLE": true, + "ROLLBACK": true, + "ROLLBACK PREPARED": true, + "SAVEPOINT": true, + "SELECT": true, + "SELECT FOR KEY SHARE": true, + "SELECT FOR NO KEY UPDATE": true, + "SELECT FOR SHARE": true, + "SELECT FOR UPDATE": true, + "SET": true, + "SET CONSTRAINTS": true, + "SHOW": true, + "START TRANSACTION": true, + "TRUNCATE TABLE": true, + "UNLISTEN": true, + "UPDATE": true, + "VACUUM": true, + "VALUES": true, +} + +var eventTriggerKnownDDLTags = map[string]bool{ + "ALTER ACCESS METHOD": true, + "ALTER AGGREGATE": true, + "ALTER CAST": true, + "ALTER COLLATION": true, + "ALTER CONSTRAINT": true, + "ALTER CONVERSION": true, + "ALTER DEFAULT PRIVILEGES": true, + "ALTER DOMAIN": true, + "ALTER EXTENSION": true, + "ALTER FOREIGN DATA WRAPPER": true, + "ALTER FOREIGN TABLE": true, + "ALTER FUNCTION": true, + "ALTER INDEX": true, + "ALTER LANGUAGE": true, + "ALTER LARGE OBJECT": true, + "ALTER MATERIALIZED VIEW": true, + "ALTER OPERATOR": true, + "ALTER OPERATOR CLASS": true, + "ALTER OPERATOR FAMILY": true, + "ALTER POLICY": true, + "ALTER PROCEDURE": true, + "ALTER PUBLICATION": true, + "ALTER ROUTINE": true, + "ALTER RULE": true, + "ALTER SCHEMA": true, + "ALTER SEQUENCE": true, + "ALTER SERVER": true, + "ALTER STATISTICS": true, + "ALTER SUBSCRIPTION": true, + "ALTER TABLE": true, + "ALTER TEXT SEARCH CONFIGURATION": true, + "ALTER TEXT SEARCH DICTIONARY": true, + "ALTER TEXT SEARCH PARSER": true, + "ALTER TEXT SEARCH TEMPLATE": true, + "ALTER TRANSFORM": true, + "ALTER TRIGGER": true, + "ALTER TYPE": true, + "ALTER USER MAPPING": true, + "ALTER VIEW": true, + "COMMENT": true, + "CREATE ACCESS METHOD": true, + "CREATE AGGREGATE": true, + "CREATE CAST": true, + "CREATE COLLATION": true, + "CREATE CONSTRAINT": true, + "CREATE CONVERSION": true, + "CREATE DOMAIN": true, + "CREATE EXTENSION": true, + "CREATE FOREIGN DATA WRAPPER": true, + "CREATE FOREIGN TABLE": true, + "CREATE FUNCTION": true, + "CREATE INDEX": true, + "CREATE LANGUAGE": true, + "CREATE MATERIALIZED VIEW": true, + "CREATE OPERATOR": true, + "CREATE OPERATOR CLASS": true, + "CREATE OPERATOR FAMILY": true, + "CREATE POLICY": true, + "CREATE PROCEDURE": true, + "CREATE PUBLICATION": true, + "CREATE ROUTINE": true, + "CREATE RULE": true, + "CREATE SCHEMA": true, + "CREATE SEQUENCE": true, + "CREATE SERVER": true, + "CREATE STATISTICS": true, + "CREATE SUBSCRIPTION": true, + "CREATE TABLE": true, + "CREATE TABLE AS": true, + "CREATE TEXT SEARCH CONFIGURATION": true, + "CREATE TEXT SEARCH DICTIONARY": true, + "CREATE TEXT SEARCH PARSER": true, + "CREATE TEXT SEARCH TEMPLATE": true, + "CREATE TRANSFORM": true, + "CREATE TRIGGER": true, + "CREATE TYPE": true, + "CREATE USER MAPPING": true, + "CREATE VIEW": true, + "DROP ACCESS METHOD": true, + "DROP AGGREGATE": true, + "DROP CAST": true, + "DROP COLLATION": true, + "DROP CONSTRAINT": true, + "DROP CONVERSION": true, + "DROP DOMAIN": true, + "DROP EXTENSION": true, + "DROP FOREIGN DATA WRAPPER": true, + "DROP FOREIGN TABLE": true, + "DROP FUNCTION": true, + "DROP INDEX": true, + "DROP LANGUAGE": true, + "DROP MATERIALIZED VIEW": true, + "DROP OPERATOR": true, + "DROP OPERATOR CLASS": true, + "DROP OPERATOR FAMILY": true, + "DROP OWNED": true, + "DROP POLICY": true, + "DROP PROCEDURE": true, + "DROP PUBLICATION": true, + "DROP ROUTINE": true, + "DROP RULE": true, + "DROP SCHEMA": true, + "DROP SEQUENCE": true, + "DROP SERVER": true, + "DROP STATISTICS": true, + "DROP SUBSCRIPTION": true, + "DROP TABLE": true, + "DROP TEXT SEARCH CONFIGURATION": true, + "DROP TEXT SEARCH DICTIONARY": true, + "DROP TEXT SEARCH PARSER": true, + "DROP TEXT SEARCH TEMPLATE": true, + "DROP TRANSFORM": true, + "DROP TRIGGER": true, + "DROP TYPE": true, + "DROP USER MAPPING": true, + "DROP VIEW": true, + "GRANT": true, + "IMPORT FOREIGN SCHEMA": true, + "LOGIN": true, + "REFRESH MATERIALIZED VIEW": true, + "REINDEX": true, + "REVOKE": true, + "SECURITY LABEL": true, + "SELECT INTO": true, +} + +var eventTriggerTableRewriteTags = map[string]bool{ + "ALTER MATERIALIZED VIEW": true, + "ALTER TABLE": true, + "ALTER TYPE": true, +} + +func (c *Catalog) findEventTriggerFunc(schemaName, funcName string) (*BuiltinProc, bool) { + foundAny := false + for _, p := range c.procByName[funcName] { + if p.NArgs != 0 { + continue + } + if schemaName != "" { + up := c.userProcs[p.OID] + if up == nil || c.schemaByName[schemaName] == nil || up.Schema.OID != c.schemaByName[schemaName].OID { + continue + } + } + foundAny = true + if p.RetType == EVENTTRIGGEROID { + return p, false + } + } + return nil, foundAny +} + +func diffEventTriggers(from, to *Catalog) []EventTriggerDiffEntry { + fromMap := make(map[string]*EventTrigger) + for _, evt := range from.eventTriggers { + fromMap[evt.Name] = evt + } + toMap := make(map[string]*EventTrigger) + for _, evt := range to.eventTriggers { + toMap[evt.Name] = evt + } + + var result []EventTriggerDiffEntry + for name, fromEvt := range fromMap { + if _, ok := toMap[name]; !ok { + result = append(result, EventTriggerDiffEntry{Action: DiffDrop, Name: name, From: fromEvt}) + } + } + for name, toEvt := range toMap { + fromEvt, ok := fromMap[name] + if !ok { + result = append(result, EventTriggerDiffEntry{Action: DiffAdd, Name: name, To: toEvt}) + continue + } + if eventTriggersChanged(from, to, fromEvt, toEvt) { + result = append(result, EventTriggerDiffEntry{Action: DiffModify, Name: name, From: fromEvt, To: toEvt}) + } + } + + sort.Slice(result, func(i, j int) bool { + if result[i].Name != result[j].Name { + return result[i].Name < result[j].Name + } + return result[i].Action < result[j].Action + }) + return result +} + +func eventTriggersChanged(from, to *Catalog, a, b *EventTrigger) bool { + if a.EventName != b.EventName || a.Enabled != b.Enabled { + return true + } + if resolveEventTriggerFuncIdentity(from, a.FuncOID) != resolveEventTriggerFuncIdentity(to, b.FuncOID) { + return true + } + return !stringSliceEqual(a.Tags, b.Tags) +} + +func resolveEventTriggerFuncIdentity(c *Catalog, funcOID uint32) string { + up := c.userProcs[funcOID] + if up != nil { + return funcIdentity(c, up) + } + p := c.procByOID[funcOID] + if p == nil { + return "" + } + return p.Name + "()" +} + +func resolveEventTriggerFuncSQLName(c *Catalog, funcOID uint32) string { + up := c.userProcs[funcOID] + if up != nil && up.Schema != nil { + return migrationQualifiedName(up.Schema.Name, up.Name) + } + p := c.procByOID[funcOID] + if p == nil { + return quoteIdentAlways("unknown_function") + } + return quoteIdentAlways(p.Name) +} + +func structuralEventTriggerChange(from, to *Catalog, a, b *EventTrigger) bool { + if a == nil || b == nil { + return true + } + if a.EventName != b.EventName { + return true + } + if resolveEventTriggerFuncIdentity(from, a.FuncOID) != resolveEventTriggerFuncIdentity(to, b.FuncOID) { + return true + } + return !stringSliceEqual(a.Tags, b.Tags) +} diff --git a/pg/catalog/event_trigger_test.go b/pg/catalog/event_trigger_test.go new file mode 100644 index 00000000..b0734dc0 --- /dev/null +++ b/pg/catalog/event_trigger_test.go @@ -0,0 +1,294 @@ +package catalog + +import ( + "strings" + "testing" +) + +const eventTriggerFuncSQL = ` +CREATE FUNCTION evt_fn() RETURNS event_trigger +LANGUAGE plpgsql AS $$ BEGIN END $$; +` + +func TestEventTriggerCatalog(t *testing.T) { + t.Run("create alter comment and drop", func(t *testing.T) { + c, err := LoadSQL(eventTriggerFuncSQL + ` + CREATE EVENT TRIGGER evt_start ON ddl_command_start + WHEN TAG IN ('create table', 'DROP TABLE') + EXECUTE FUNCTION evt_fn(); +ALTER EVENT TRIGGER evt_start ENABLE ALWAYS; +COMMENT ON EVENT TRIGGER evt_start IS 'event trigger comment'; +`) + if err != nil { + t.Fatal(err) + } + + evt := c.eventTriggerByName["evt_start"] + if evt == nil { + t.Fatal("expected event trigger evt_start to be tracked") + } + if evt.EventName != "ddl_command_start" { + t.Fatalf("expected ddl_command_start, got %q", evt.EventName) + } + if evt.Enabled != 'A' { + t.Fatalf("expected enabled state A, got %q", evt.Enabled) + } + if len(evt.Tags) != 2 || evt.Tags[0] != "CREATE TABLE" || evt.Tags[1] != "DROP TABLE" { + t.Fatalf("expected canonical tags, got %#v", evt.Tags) + } + if got := c.comments[commentKey{ObjType: 'E', ObjOID: evt.OID}]; got != "event trigger comment" { + t.Fatalf("expected event trigger comment, got %q", got) + } + + if _, err := c.Exec("DROP EVENT TRIGGER evt_start", nil); err != nil { + t.Fatal(err) + } + if c.eventTriggerByName["evt_start"] != nil { + t.Fatal("expected event trigger to be removed after DROP") + } + }) + + t.Run("accepts postgres event trigger command tags", func(t *testing.T) { + c, err := LoadSQL(eventTriggerFuncSQL + ` + CREATE EVENT TRIGGER evt_start ON ddl_command_start + WHEN TAG IN ( + 'REFRESH MATERIALIZED VIEW', + 'REINDEX', + 'SECURITY LABEL', + 'REVOKE', + 'SELECT INTO', + 'DROP CONSTRAINT', + 'LOGIN' + ) + EXECUTE FUNCTION evt_fn(); + `) + if err != nil { + t.Fatal(err) + } + evt := c.eventTriggerByName["evt_start"] + if evt == nil { + t.Fatal("expected event trigger evt_start to be tracked") + } + want := []string{ + "REFRESH MATERIALIZED VIEW", + "REINDEX", + "SECURITY LABEL", + "REVOKE", + "SELECT INTO", + "DROP CONSTRAINT", + "LOGIN", + } + if strings.Join(evt.Tags, ",") != strings.Join(want, ",") { + t.Fatalf("expected postgres command tags %#v, got %#v", want, evt.Tags) + } + }) + + t.Run("accepts postgres table rewrite command tags", func(t *testing.T) { + c, err := LoadSQL(eventTriggerFuncSQL + ` + CREATE EVENT TRIGGER evt_rewrite ON table_rewrite + WHEN TAG IN ('ALTER MATERIALIZED VIEW', 'ALTER TABLE', 'ALTER TYPE') + EXECUTE FUNCTION evt_fn(); + `) + if err != nil { + t.Fatal(err) + } + evt := c.eventTriggerByName["evt_rewrite"] + if evt == nil { + t.Fatal("expected event trigger evt_rewrite to be tracked") + } + if len(evt.Tags) != 3 || evt.Tags[0] != "ALTER MATERIALIZED VIEW" || evt.Tags[1] != "ALTER TABLE" || evt.Tags[2] != "ALTER TYPE" { + t.Fatalf("expected table rewrite tags, got %#v", evt.Tags) + } + }) + + t.Run("rejects postgres incompatible definitions", func(t *testing.T) { + tests := []struct { + name string + sql string + wantErr string + }{ + { + name: "invalid event name", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON elephant_bootstrap EXECUTE FUNCTION evt_fn();`, + wantErr: `unrecognized event name "elephant_bootstrap"`, + }, + { + name: "trigger function return type", + sql: ` +CREATE FUNCTION trg_fn() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RETURN NEW; END $$; +CREATE EVENT TRIGGER bad ON ddl_command_start EXECUTE FUNCTION trg_fn(); +`, + wantErr: `function trg_fn must return type event_trigger`, + }, + { + name: "duplicate tag filter", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON ddl_command_start WHEN TAG IN ('CREATE TABLE') AND TAG IN ('DROP TABLE') EXECUTE FUNCTION evt_fn();`, + wantErr: `filter variable "tag" specified more than once`, + }, + { + name: "unknown filter variable", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON ddl_command_start WHEN foo IN ('CREATE TABLE') EXECUTE FUNCTION evt_fn();`, + wantErr: `unrecognized filter variable "foo"`, + }, + { + name: "unsupported ddl tag", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON ddl_command_start WHEN TAG IN ('DROP EVENT TRIGGER') EXECUTE FUNCTION evt_fn();`, + wantErr: `event triggers are not supported for DROP EVENT TRIGGER`, + }, + { + name: "unknown ddl tag", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON ddl_command_start WHEN TAG IN ('sandwich') EXECUTE FUNCTION evt_fn();`, + wantErr: `filter value "SANDWICH" not recognized for filter variable "tag"`, + }, + { + name: "login tag filter", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON login WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION evt_fn();`, + wantErr: `tag filtering is not supported for login event triggers`, + }, + { + name: "table rewrite rejects create table", + sql: eventTriggerFuncSQL + `CREATE EVENT TRIGGER bad ON table_rewrite WHEN TAG IN ('CREATE TABLE') EXECUTE FUNCTION evt_fn();`, + wantErr: `event triggers are not supported for CREATE TABLE`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadSQL(tt.sql) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %v", tt.wantErr, err) + } + }) + } + }) +} + +func TestEventTriggerSDL(t *testing.T) { + c, err := LoadSDL(` +COMMENT ON EVENT TRIGGER evt_start IS 'event trigger comment'; +ALTER EVENT TRIGGER evt_start DISABLE; +CREATE EVENT TRIGGER evt_start ON ddl_command_start + WHEN TAG IN ('CREATE TABLE') + EXECUTE FUNCTION evt_fn(); +CREATE FUNCTION evt_fn() RETURNS event_trigger +LANGUAGE plpgsql AS $$ BEGIN END $$; +`) + if err != nil { + t.Fatal(err) + } + evt := c.eventTriggerByName["evt_start"] + if evt == nil { + t.Fatal("expected event trigger from SDL") + } + if evt.Enabled != 'D' { + t.Fatalf("expected disabled event trigger, got %q", evt.Enabled) + } + if got := c.comments[commentKey{ObjType: 'E', ObjOID: evt.OID}]; got != "event trigger comment" { + t.Fatalf("expected event trigger comment, got %q", got) + } +} + +func TestEventTriggerDiffAndMigration(t *testing.T) { + before := eventTriggerFuncSQL + ` +CREATE EVENT TRIGGER evt_start ON ddl_command_start EXECUTE FUNCTION evt_fn(); +` + after := eventTriggerFuncSQL + ` +CREATE EVENT TRIGGER evt_start ON ddl_command_start + WHEN TAG IN ('CREATE TABLE') + EXECUTE FUNCTION evt_fn(); +ALTER EVENT TRIGGER evt_start ENABLE REPLICA; +COMMENT ON EVENT TRIGGER evt_start IS 'event trigger comment'; +` + + from, err := LoadSDL(before) + if err != nil { + t.Fatal(err) + } + to, err := LoadSDL(after) + if err != nil { + t.Fatal(err) + } + diff := Diff(from, to) + if len(diff.EventTriggers) != 1 { + t.Fatalf("expected 1 event trigger diff, got %d", len(diff.EventTriggers)) + } + if diff.EventTriggers[0].Action != DiffModify { + t.Fatalf("expected event trigger modify diff, got %d", diff.EventTriggers[0].Action) + } + if len(diff.Comments) != 1 { + t.Fatalf("expected 1 comment diff, got %d", len(diff.Comments)) + } + + plan := GenerateMigration(from, to, diff) + sql := plan.SQL() + if !strings.Contains(sql, "DROP EVENT TRIGGER") || !strings.Contains(sql, "CREATE EVENT TRIGGER") { + t.Fatalf("expected drop/create event trigger migration, got:\n%s", sql) + } + if !strings.Contains(sql, "ALTER EVENT TRIGGER") || !strings.Contains(sql, "ENABLE REPLICA") { + t.Fatalf("expected enable replica migration, got:\n%s", sql) + } + if !strings.Contains(sql, "COMMENT ON EVENT TRIGGER") { + t.Fatalf("expected event trigger comment migration, got:\n%s", sql) + } + + migrated, err := LoadSQL(before + ";\n" + sql) + if err != nil { + t.Fatalf("migration failed: %v\nSQL:\n%s", err, sql) + } + if diff2 := Diff(migrated, to); !diff2.IsEmpty() { + t.Fatalf("expected empty roundtrip diff, got %#v", diff2) + } +} + +func TestEventTriggerFunctionDependency(t *testing.T) { + c, err := LoadSQL(eventTriggerFuncSQL + ` +CREATE EVENT TRIGGER evt_start ON ddl_command_start EXECUTE FUNCTION evt_fn(); +`) + if err != nil { + t.Fatal(err) + } + results, err := c.Exec("DROP FUNCTION evt_fn()", nil) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 || results[0].Error == nil { + t.Fatalf("expected DROP FUNCTION to be blocked by event trigger dependency, got %#v", results) + } + results, err = c.Exec("DROP FUNCTION evt_fn() CASCADE", nil) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 || results[0].Error != nil { + t.Fatalf("expected DROP FUNCTION CASCADE to succeed, got %#v", results) + } + if len(c.eventTriggers) != 0 { + t.Fatalf("expected cascade to drop event trigger, got %d", len(c.eventTriggers)) + } +} + +func TestEventTriggerFunctionDefinitionValidation(t *testing.T) { + tests := []struct { + name string + sql string + wantErr string + }{ + { + name: "sql functions cannot return event_trigger", + sql: `CREATE FUNCTION evt_sql_fn() RETURNS event_trigger LANGUAGE sql AS $$ SELECT 1 $$;`, + wantErr: `SQL functions cannot return type event_trigger`, + }, + { + name: "event trigger functions cannot have arguments", + sql: `CREATE FUNCTION evt_arg_fn(name text) RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN END $$;`, + wantErr: `event trigger functions cannot have declared arguments`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := LoadSQL(tt.sql) + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("expected error containing %q, got %v", tt.wantErr, err) + } + }) + } +} diff --git a/pg/catalog/functioncmds.go b/pg/catalog/functioncmds.go index 22ad6938..e4fdb69b 100644 --- a/pg/catalog/functioncmds.go +++ b/pg/catalog/functioncmds.go @@ -455,6 +455,15 @@ func (c *Catalog) CreateFunctionStmt(stmt *nodes.CreateFunctionStmt) error { returnSet = true } + if retOID == EVENTTRIGGEROID { + if strings.EqualFold(language, "sql") { + return errInvalidFunctionDefinition("SQL functions cannot return type event_trigger") + } + if len(allArgTypes) > 0 { + return errInvalidFunctionDefinition("event trigger functions cannot have declared arguments") + } + } + // --------------------------------------------------------------- // ROWS vs returnsSet validation. // pg: src/backend/commands/functioncmds.c (line 1248-1251) diff --git a/pg/catalog/migration.go b/pg/catalog/migration.go index d1be0601..ea1157fd 100644 --- a/pg/catalog/migration.go +++ b/pg/catalog/migration.go @@ -20,62 +20,66 @@ const ( // Priority constants for tie-breaking within a phase during topological sort. // Lower values are executed earlier. const ( - PrioritySchema = 0 - PriorityExtension = 1 - PriorityType = 2 // Enum/Domain/Range/Composite - PrioritySequence = 3 - PriorityFunction = 4 - PriorityTable = 5 - PriorityColumn = 6 // uses parent table OID - PriorityConstraint = 7 // non-FK - PriorityView = 8 - PriorityIndex = 9 - PriorityTrigger = 10 - PriorityPolicy = 11 - PriorityMetadata = 12 // Comment/Grant/Revoke - PriorityFKDeferred = 99 // FK constraint (PhasePost) + PrioritySchema = 0 + PriorityExtension = 1 + PriorityType = 2 // Enum/Domain/Range/Composite + PrioritySequence = 3 + PriorityFunction = 4 + PriorityTable = 5 + PriorityColumn = 6 // uses parent table OID + PriorityConstraint = 7 // non-FK + PriorityView = 8 + PriorityIndex = 9 + PriorityTrigger = 10 + PriorityEventTrigger = 10 + PriorityPolicy = 11 + PriorityMetadata = 12 // Comment/Grant/Revoke + PriorityFKDeferred = 99 // FK constraint (PhasePost) ) // MigrationOpType classifies a single DDL operation. type MigrationOpType string const ( - OpCreateSchema MigrationOpType = "CreateSchema" - OpDropSchema MigrationOpType = "DropSchema" - OpAlterSchema MigrationOpType = "AlterSchema" - OpCreateTable MigrationOpType = "CreateTable" - OpDropTable MigrationOpType = "DropTable" - OpAddColumn MigrationOpType = "AddColumn" - OpDropColumn MigrationOpType = "DropColumn" - OpAlterColumn MigrationOpType = "AlterColumn" - OpAddConstraint MigrationOpType = "AddConstraint" - OpDropConstraint MigrationOpType = "DropConstraint" - OpCreateIndex MigrationOpType = "CreateIndex" - OpDropIndex MigrationOpType = "DropIndex" - OpCreateSequence MigrationOpType = "CreateSequence" - OpDropSequence MigrationOpType = "DropSequence" - OpAlterSequence MigrationOpType = "AlterSequence" - OpCreateFunction MigrationOpType = "CreateFunction" - OpDropFunction MigrationOpType = "DropFunction" - OpAlterFunction MigrationOpType = "AlterFunction" - OpCreateType MigrationOpType = "CreateType" - OpDropType MigrationOpType = "DropType" - OpAlterType MigrationOpType = "AlterType" - OpCreateTrigger MigrationOpType = "CreateTrigger" - OpDropTrigger MigrationOpType = "DropTrigger" - OpCreateView MigrationOpType = "CreateView" - OpDropView MigrationOpType = "DropView" - OpAlterView MigrationOpType = "AlterView" - OpCreateExtension MigrationOpType = "CreateExtension" - OpDropExtension MigrationOpType = "DropExtension" - OpAlterExtension MigrationOpType = "AlterExtension" - OpCreatePolicy MigrationOpType = "CreatePolicy" - OpDropPolicy MigrationOpType = "DropPolicy" - OpAlterPolicy MigrationOpType = "AlterPolicy" - OpAlterTable MigrationOpType = "AlterTable" - OpComment MigrationOpType = "Comment" - OpGrant MigrationOpType = "Grant" - OpRevoke MigrationOpType = "Revoke" + OpCreateSchema MigrationOpType = "CreateSchema" + OpDropSchema MigrationOpType = "DropSchema" + OpAlterSchema MigrationOpType = "AlterSchema" + OpCreateTable MigrationOpType = "CreateTable" + OpDropTable MigrationOpType = "DropTable" + OpAddColumn MigrationOpType = "AddColumn" + OpDropColumn MigrationOpType = "DropColumn" + OpAlterColumn MigrationOpType = "AlterColumn" + OpAddConstraint MigrationOpType = "AddConstraint" + OpDropConstraint MigrationOpType = "DropConstraint" + OpCreateIndex MigrationOpType = "CreateIndex" + OpDropIndex MigrationOpType = "DropIndex" + OpCreateSequence MigrationOpType = "CreateSequence" + OpDropSequence MigrationOpType = "DropSequence" + OpAlterSequence MigrationOpType = "AlterSequence" + OpCreateFunction MigrationOpType = "CreateFunction" + OpDropFunction MigrationOpType = "DropFunction" + OpAlterFunction MigrationOpType = "AlterFunction" + OpCreateType MigrationOpType = "CreateType" + OpDropType MigrationOpType = "DropType" + OpAlterType MigrationOpType = "AlterType" + OpCreateTrigger MigrationOpType = "CreateTrigger" + OpDropTrigger MigrationOpType = "DropTrigger" + OpCreateEventTrigger MigrationOpType = "CreateEventTrigger" + OpDropEventTrigger MigrationOpType = "DropEventTrigger" + OpAlterEventTrigger MigrationOpType = "AlterEventTrigger" + OpCreateView MigrationOpType = "CreateView" + OpDropView MigrationOpType = "DropView" + OpAlterView MigrationOpType = "AlterView" + OpCreateExtension MigrationOpType = "CreateExtension" + OpDropExtension MigrationOpType = "DropExtension" + OpAlterExtension MigrationOpType = "AlterExtension" + OpCreatePolicy MigrationOpType = "CreatePolicy" + OpDropPolicy MigrationOpType = "DropPolicy" + OpAlterPolicy MigrationOpType = "AlterPolicy" + OpAlterTable MigrationOpType = "AlterTable" + OpComment MigrationOpType = "Comment" + OpGrant MigrationOpType = "Grant" + OpRevoke MigrationOpType = "Revoke" ) // MigrationOp represents a single DDL operation in a migration plan. @@ -216,6 +220,7 @@ func GenerateMigration(from, to *Catalog, diff *SchemaDiff) *MigrationPlan { ops = append(ops, generateIndexDDL(from, to, diff)...) ops = append(ops, generatePartitionDDL(from, to, diff)...) ops = append(ops, generateTriggerDDL(from, to, diff)...) + ops = append(ops, generateEventTriggerDDL(from, to, diff)...) ops = append(ops, generatePolicyDDL(from, to, diff)...) ops = append(ops, generateCommentDDL(from, to, diff)...) ops = append(ops, generateGrantDDL(from, to, diff)...) @@ -230,7 +235,6 @@ func GenerateMigration(from, to *Catalog, diff *SchemaDiff) *MigrationPlan { return &MigrationPlan{Ops: ops} } - // wrapColumnTypeChangesWithViewOps detects ALTER COLUMN TYPE operations and // injects synthetic DROP VIEW + CREATE VIEW ops for any views that depend on // the modified table (via catalog deps). PG cannot alter a column type if a @@ -725,4 +729,3 @@ func sortMigrationOps(from, to *Catalog, ops []MigrationOp) []MigrationOp { result = append(result, postOps...) return result } - diff --git a/pg/catalog/migration_comment.go b/pg/catalog/migration_comment.go index b156ecc1..6361ac73 100644 --- a/pg/catalog/migration_comment.go +++ b/pg/catalog/migration_comment.go @@ -115,6 +115,8 @@ func commentObjectTarget(c *Catalog, objType byte, objDescription string, subID return formatConstraintCommentTarget(objDescription) case 'g': // trigger (schema.table.trigger) return formatTriggerCommentTarget(objDescription) + case 'E': // event trigger + return fmt.Sprintf("EVENT TRIGGER %s", quoteIdentAlways(objDescription)) case 'p': // policy (schema.table.policy) return formatPolicyCommentTarget(objDescription) default: diff --git a/pg/catalog/migration_event_trigger.go b/pg/catalog/migration_event_trigger.go new file mode 100644 index 00000000..719614cf --- /dev/null +++ b/pg/catalog/migration_event_trigger.go @@ -0,0 +1,128 @@ +package catalog + +import ( + "fmt" + "sort" + "strings" +) + +func generateEventTriggerDDL(from, to *Catalog, diff *SchemaDiff) []MigrationOp { + var ops []MigrationOp + for _, entry := range diff.EventTriggers { + switch entry.Action { + case DiffAdd: + if entry.To != nil { + ops = append(ops, buildCreateEventTriggerOps(to, entry.To)...) + } + case DiffDrop: + if entry.From != nil { + ops = append(ops, buildDropEventTriggerOp(entry.From)) + } + case DiffModify: + if structuralEventTriggerChange(from, to, entry.From, entry.To) { + if entry.From != nil { + ops = append(ops, buildDropEventTriggerOp(entry.From)) + } + if entry.To != nil { + ops = append(ops, buildCreateEventTriggerOps(to, entry.To)...) + } + } else if entry.To != nil { + ops = append(ops, buildAlterEventTriggerEnableOp(entry.To)) + } + } + } + + sort.Slice(ops, func(i, j int) bool { + if ops[i].ObjectName != ops[j].ObjectName { + return ops[i].ObjectName < ops[j].ObjectName + } + return eventTriggerOpRank(ops[i].Type) < eventTriggerOpRank(ops[j].Type) + }) + return ops +} + +func eventTriggerOpRank(t MigrationOpType) int { + switch t { + case OpDropEventTrigger: + return 0 + case OpCreateEventTrigger: + return 1 + case OpAlterEventTrigger: + return 2 + default: + return 3 + } +} + +func buildDropEventTriggerOp(evt *EventTrigger) MigrationOp { + return MigrationOp{ + Type: OpDropEventTrigger, + ObjectName: evt.Name, + SQL: fmt.Sprintf("DROP EVENT TRIGGER %s", quoteIdentAlways(evt.Name)), + Transactional: true, + Phase: PhasePre, + ObjType: 'E', + ObjOID: evt.OID, + Priority: PriorityEventTrigger, + } +} + +func buildCreateEventTriggerOps(c *Catalog, evt *EventTrigger) []MigrationOp { + var ops []MigrationOp + var b strings.Builder + b.WriteString("CREATE EVENT TRIGGER ") + b.WriteString(quoteIdentAlways(evt.Name)) + b.WriteString(" ON ") + b.WriteString(quoteIdentAlways(evt.EventName)) + if len(evt.Tags) > 0 { + var tags []string + for _, tag := range evt.Tags { + tags = append(tags, quoteLiteral(tag)) + } + b.WriteString("\n WHEN TAG IN (") + b.WriteString(strings.Join(tags, ", ")) + b.WriteString(")") + } + b.WriteString("\n EXECUTE FUNCTION ") + b.WriteString(resolveEventTriggerFuncSQLName(c, evt.FuncOID)) + b.WriteString("()") + + ops = append(ops, MigrationOp{ + Type: OpCreateEventTrigger, + ObjectName: evt.Name, + SQL: b.String(), + Transactional: true, + Phase: PhaseMain, + ObjType: 'E', + ObjOID: evt.OID, + Priority: PriorityEventTrigger, + }) + if evt.Enabled != 'O' { + ops = append(ops, buildAlterEventTriggerEnableOp(evt)) + } + return ops +} + +func buildAlterEventTriggerEnableOp(evt *EventTrigger) MigrationOp { + var action string + switch evt.Enabled { + case 'D': + action = "DISABLE" + case 'A': + action = "ENABLE ALWAYS" + case 'R': + action = "ENABLE REPLICA" + default: + action = "ENABLE" + } + return MigrationOp{ + Type: OpAlterEventTrigger, + ObjectName: evt.Name, + SQL: fmt.Sprintf("ALTER EVENT TRIGGER %s %s", quoteIdentAlways(evt.Name), action), + Transactional: true, + Phase: PhaseMain, + ObjType: 'E', + ObjOID: evt.OID, + Priority: PriorityEventTrigger, + } +} diff --git a/pg/catalog/round6_test.go b/pg/catalog/round6_test.go index e75a7d3a..1527bc80 100644 --- a/pg/catalog/round6_test.go +++ b/pg/catalog/round6_test.go @@ -755,8 +755,6 @@ func TestProcessUtilityNoOpDDL(t *testing.T) { &nodes.AlterSubscriptionStmt{}, &nodes.DropSubscriptionStmt{}, &nodes.CreateTransformStmt{}, - &nodes.CreateEventTrigStmt{}, - &nodes.AlterEventTrigStmt{}, } for _, stmt := range noOpStmts { diff --git a/pg/catalog/sdl.go b/pg/catalog/sdl.go index aa465f32..cf8b58c3 100644 --- a/pg/catalog/sdl.go +++ b/pg/catalog/sdl.go @@ -125,6 +125,10 @@ func collectDeclaredObjects(stmts []nodes.Node) map[string]bool { declared[qualifiedNameFromList(s.TypeName)] = true case *nodes.CreateFunctionStmt: declared[functionIdentity(s)] = true + case *nodes.CreateEventTrigStmt: + if s.Trigname != "" { + declared["event_trigger:"+s.Trigname] = true + } case *nodes.CreateExtensionStmt: // Extensions are identified by name only. if s.Extname != "" { @@ -366,9 +370,11 @@ func stmtPriority(stmt nodes.Node) int { return 7 case *nodes.CreateTrigStmt: return 7 + case *nodes.CreateEventTrigStmt: + return 7 case *nodes.CreatePolicyStmt: return 7 - case *nodes.AlterSeqStmt: + case *nodes.AlterSeqStmt, *nodes.AlterEventTrigStmt: return 8 case *nodes.AlterTableStmt: return 9 @@ -412,6 +418,10 @@ func stmtName(stmt nodes.Node) string { return qualifiedNameFromList(s.TypeName) case *nodes.CreateFunctionStmt: return functionIdentity(s) + case *nodes.CreateEventTrigStmt: + if s.Trigname != "" { + return "event_trigger:" + s.Trigname + } case *nodes.CreateExtensionStmt: return "extension:" + s.Extname case *nodes.CreateTableAsStmt: @@ -573,6 +583,12 @@ func extractRefs(stmt nodes.Node, declared map[string]bool) (refs []string, fkRe fr, rr, tr := collectExprDepsFromTriggerStmt(s) addExprDeps(fr, rr, tr) + case *nodes.CreateEventTrigStmt: + if s.Funcname != nil { + funcName := qualifiedNameFromList(s.Funcname) + addRef(funcName + "()") + } + case *nodes.CreatePolicyStmt: if s.Table != nil { addRef(qualifiedRangeVar(s.Table)) @@ -632,9 +648,13 @@ func extractRefs(stmt nodes.Node, declared map[string]bool) (refs []string, fkRe addRef(qualifiedRangeVar(s.Relation)) } + case *nodes.AlterEventTrigStmt: + addRef("event_trigger:" + s.Trigname) + case *nodes.CommentStmt: - // Comment targets are complex; for now just note that comments - // go in the last priority layer so they execute after everything. + if s.Objtype == nodes.OBJECT_EVENT_TRIGGER { + addRef("event_trigger:" + extractSimpleObjectName(s.Object)) + } case *nodes.GrantStmt: // Grants go in the last priority layer. @@ -952,6 +972,8 @@ func validateSDLStmt(stmt nodes.Node) error { return nil case *nodes.CreateTrigStmt: return nil + case *nodes.CreateEventTrigStmt: + return nil case *nodes.CreatePolicyStmt: return nil case *nodes.CreateTableAsStmt: @@ -966,6 +988,8 @@ func validateSDLStmt(stmt nodes.Node) error { return nil case *nodes.AlterSeqStmt: return nil + case *nodes.AlterEventTrigStmt: + return nil case *nodes.AlterEnumStmt: return nil case *nodes.VariableSetStmt: diff --git a/pg/catalog/utility.go b/pg/catalog/utility.go index ece28037..0f5a1a8d 100644 --- a/pg/catalog/utility.go +++ b/pg/catalog/utility.go @@ -147,11 +147,9 @@ func (c *Catalog) ProcessUtility(stmt nodes.Node) error { // CREATE TRANSFORM: no-op for pgddl return nil case *nodes.CreateEventTrigStmt: - // CREATE EVENT TRIGGER: no-op for pgddl - return nil + return c.CreateEventTriggerStmt(s) case *nodes.AlterEventTrigStmt: - // ALTER EVENT TRIGGER: no-op for pgddl - return nil + return c.AlterEventTrigger(s) case *nodes.VariableSetStmt: // pg: src/backend/tcop/utility.c — standard_ProcessUtility (VariableSetStmt) return c.processVariableSet(s)