Files
2026-07-13 12:32:25 +08:00

242 lines
7.7 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package analyzer
import (
"fmt"
"sort"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/analyzer"
"github.com/dolthub/go-mysql-server/sql/plan"
"github.com/dolthub/go-mysql-server/sql/transform"
"github.com/dolthub/doltgresql/core"
"github.com/dolthub/doltgresql/core/id"
"github.com/dolthub/doltgresql/core/triggers"
pgexprs "github.com/dolthub/doltgresql/server/expression"
pgnodes "github.com/dolthub/doltgresql/server/node"
pgtransform "github.com/dolthub/doltgresql/server/transform"
)
// AssignTriggers assigns triggers wherever they're needed.
func AssignTriggers(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
return pgtransform.NodeWithOpaque(ctx, node, func(ctx *sql.Context, node sql.Node) (sql.Node, transform.TreeIdentity, error) {
switch node := node.(type) {
case *plan.DeleteFrom, *plan.InsertInto, *plan.Truncate, *plan.Update:
sch, beforeTrigs, afterTrigs, err := getTriggerInformation(ctx, node)
if err != nil {
return nil, transform.NewTree, err
}
if len(beforeTrigs) == 0 && len(afterTrigs) == 0 {
return node, transform.SameTree, nil
}
newNode := node
if len(beforeTrigs) > 0 {
handling := getTriggerRowHandling(node)
newNode, err = nodeWithTriggers(ctx, newNode, &pgnodes.TriggerExecution{
Timing: triggers.TriggerTiming_Before,
Triggers: beforeTrigs,
Split: handling,
Return: handling,
Sch: sch,
Source: getTriggerSource(node),
Runner: pgexprs.StatementRunner{Runner: a.Runner},
})
if err != nil {
return nil, transform.NewTree, err
}
}
if len(afterTrigs) > 0 {
newNode = &pgnodes.TriggerExecution{
Timing: triggers.TriggerTiming_After,
Triggers: afterTrigs,
Split: getTriggerRowHandling(node),
Return: pgnodes.TriggerExecutionRowHandling_None,
Sch: sch,
Source: newNode,
Runner: pgexprs.StatementRunner{Runner: a.Runner},
}
}
return newNode, transform.NewTree, nil
default:
return node, transform.SameTree, nil
}
})
}
// getTriggerInformation loads information that is common for the different trigger types.
func getTriggerInformation(ctx *sql.Context, node sql.Node) (sch sql.Schema, beforeTrigs []triggers.Trigger, afterTrigs []triggers.Trigger, err error) {
var tbl sql.Table
switch node := node.(type) {
case *plan.DeleteFrom:
tbl, err = plan.GetDeletable(node.Child)
if err != nil {
return nil, nil, nil, err
}
case *plan.InsertInto:
tbl, err = plan.GetInsertable(node.Destination)
if err != nil {
return nil, nil, nil, err
}
case *plan.Truncate:
tbl, err = plan.GetTruncatable(node.Child)
if err != nil {
return nil, nil, nil, err
}
case *plan.Update:
// TODO: If there is a JoinNode in here, then don't bother calling GetUpdatable, because
// it doesn't currently return a type that can be used to query trigger information.
// We need to rework the plan.GetUpdatable() API to support returning multiple
// update targets and to return types that are compatible with the interfaces
// Doltgres needs in order to populate trigger information.
if hasJoinNode(node) {
return nil, nil, nil, nil
}
tbl, err = plan.GetUpdatable(node.Child)
if err != nil {
return nil, nil, nil, err
}
default:
return nil, nil, nil, nil
}
dbName := ctx.GetCurrentDatabase()
// TODO: some dolt tables don't implement this interface, so we use the current db for now
// An alternative would be to get the resolved table and use the db there.
schTbl, ok := tbl.(sql.DatabaseSchemaTable)
if ok {
dbName = schTbl.DatabaseSchema().Name()
}
trigCollection, err := core.GetTriggersCollectionFromContext(ctx, dbName)
if err != nil {
return nil, nil, nil, err
}
tblID, ok, _ := id.GetFromTable(ctx, tbl)
if !ok {
return nil, nil, nil, nil
}
allTrigs := trigCollection.GetTriggersForTable(ctx, tblID)
// Return early if there are no triggers for the table
if len(allTrigs) == 0 {
return tbl.Schema(ctx), nil, nil, nil
}
// Trigger order is determined by the name
sort.Slice(allTrigs, func(i, j int) bool {
return allTrigs[i].ID.TriggerName() < allTrigs[j].ID.TriggerName()
})
beforeTrigs = make([]triggers.Trigger, 0, len(allTrigs))
afterTrigs = make([]triggers.Trigger, 0, len(allTrigs))
for _, trig := range allTrigs {
matchesEventType := false
for _, event := range trig.Events {
switch node.(type) {
case *plan.DeleteFrom:
if event.Type == triggers.TriggerEventType_Delete {
matchesEventType = true
}
case *plan.InsertInto:
if event.Type == triggers.TriggerEventType_Insert {
matchesEventType = true
}
case *plan.Truncate:
if event.Type == triggers.TriggerEventType_Truncate {
matchesEventType = true
}
case *plan.Update:
if event.Type == triggers.TriggerEventType_Update {
matchesEventType = true
}
}
}
if !matchesEventType {
continue
}
switch trig.Timing {
case triggers.TriggerTiming_Before:
beforeTrigs = append(beforeTrigs, trig)
case triggers.TriggerTiming_After:
afterTrigs = append(afterTrigs, trig)
default:
return nil, nil, nil, fmt.Errorf("trigger timing has not yet been implemented")
}
}
return tbl.Schema(ctx), beforeTrigs, afterTrigs, nil
}
// hasJoinNode returns true if |node| or any child is a JoinNode.
func hasJoinNode(node sql.Node) bool {
updateJoinFound := false
transform.Inspect(node, func(n sql.Node) bool {
if _, ok := n.(*plan.JoinNode); ok {
updateJoinFound = true
}
return !updateJoinFound
})
return updateJoinFound
}
// getTriggerSource returns the trigger's source node.
func getTriggerSource(node sql.Node) sql.Node {
switch node := node.(type) {
case *plan.DeleteFrom:
return node.Child
case *plan.InsertInto:
return node.Source
case *plan.Truncate:
return node.Child
case *plan.Update:
return node.Child
default:
return node
}
}
// getTriggerRowHandling returns the trigger's row handling type (based on how GMS passes rows in the intermediate
// steps).
func getTriggerRowHandling(node sql.Node) pgnodes.TriggerExecutionRowHandling {
switch node.(type) {
case *plan.DeleteFrom:
return pgnodes.TriggerExecutionRowHandling_Old
case *plan.InsertInto:
return pgnodes.TriggerExecutionRowHandling_New
case *plan.Truncate:
return pgnodes.TriggerExecutionRowHandling_None
case *plan.Update:
return pgnodes.TriggerExecutionRowHandling_OldNew
default:
return pgnodes.TriggerExecutionRowHandling_None
}
}
// nodeWithTriggers calls the appropriate WithX function depending on the node type.
func nodeWithTriggers(ctx *sql.Context, node sql.Node, executionNode *pgnodes.TriggerExecution) (sql.Node, error) {
switch node := node.(type) {
case *plan.DeleteFrom:
return node.WithChildren(ctx, executionNode)
case *plan.InsertInto:
return node.WithSource(executionNode), nil
case *plan.Truncate:
return node.WithChildren(ctx, executionNode)
case *plan.Update:
return node.WithChildren(ctx, executionNode)
default:
return nil, fmt.Errorf("unknown node for triggers")
}
}