package platform import ( "database/sql" "errors" "net/http" ) type subgraphRelation struct { Parent string `json:"parent_board_id"` Frame string `json:"frame_id"` Child string `json:"child_board_id"` Archived bool `json:"archived"` } type subgraphRelationChange struct { Before subgraphRelation `json:"before"` After subgraphRelation `json:"after"` } func readSubgraphRelation(r *http.Request, tx *sql.Tx, project, workspace, child string) (subgraphRelation, error) { result := subgraphRelation{Child: child} err := tx.QueryRowContext(r.Context(), `SELECT parent_board_id::text,frame_id,archived_at IS NOT NULL FROM canvas_subgraphs WHERE project=$1 AND workspace_id=$2 AND child_board_id=$3 FOR UPDATE`, project, workspace, child).Scan(&result.Parent, &result.Frame, &result.Archived) return result, err } func writeSubgraphRelation(r *http.Request, tx *sql.Tx, project, workspace string, relation subgraphRelation) error { before, err := readSubgraphRelation(r, tx, project, workspace, relation.Child) if err != nil { return err } if before != relation { if !relation.Archived { if err = validateSubgraphPlacement(r, tx, project, workspace, relation.Parent, relation.Child); err != nil { return err } } if err = invalidateCanvasAISubtree(r, tx, project, workspace, relation.Child); err != nil { return err } } _, err = tx.ExecContext(r.Context(), `UPDATE canvas_subgraphs SET parent_board_id=$4,frame_id=$5,archived_at=CASE WHEN $6 THEN COALESCE(archived_at,now()) ELSE NULL END WHERE project=$1 AND workspace_id=$2 AND child_board_id=$3`, project, workspace, relation.Child, relation.Parent, relation.Frame, relation.Archived) return err } var errSubgraphCycle = errors.New("subgraph_cycle") var errSubgraphDepth = errors.New("subgraph_depth_exceeded") func validateSubgraphPlacement(r *http.Request, tx *sql.Tx, project, workspace, parent, child string) error { var cycle bool var depth int err := tx.QueryRowContext(r.Context(), `WITH RECURSIVE ancestors(id,depth) AS( SELECT $3::uuid,0 UNION ALL SELECT s.parent_board_id,a.depth+1 FROM ancestors a JOIN canvas_subgraphs s ON s.project=$1 AND s.workspace_id=$2 AND s.child_board_id=a.id AND s.archived_at IS NULL WHERE a.depth<8 ), descendants(id,depth) AS( SELECT $4::uuid,0 UNION ALL SELECT s.child_board_id,d.depth+1 FROM descendants d JOIN canvas_subgraphs s ON s.project=$1 AND s.workspace_id=$2 AND s.parent_board_id=d.id AND s.archived_at IS NULL WHERE d.depth<8 ) SELECT EXISTS(SELECT 1 FROM ancestors WHERE id=$4::uuid), (SELECT max(depth) FROM ancestors)+1+(SELECT max(depth) FROM descendants)`, project, workspace, parent, child).Scan(&cycle, &depth) if err != nil { return err } if cycle { return errSubgraphCycle } if depth > 8 { return errSubgraphDepth } return nil }