chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,795 @@
|
||||
// Copyright 2025 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 plpgsql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
)
|
||||
|
||||
// action exists to match the expected JSON format.
|
||||
type action struct {
|
||||
StmtBlock plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"`
|
||||
}
|
||||
|
||||
// cond exists to match the expected JSON format.
|
||||
type cond struct {
|
||||
Expression plpgSQL_expr `json:"PLpgSQL_expr"`
|
||||
}
|
||||
|
||||
// datatype exists to match the expected JSON format.
|
||||
type datatype struct {
|
||||
Type plpgSQL_type `json:"PLpgSQL_type"`
|
||||
}
|
||||
|
||||
// default_val exists to match the expected JSON format.
|
||||
type default_val struct {
|
||||
Var plpgSQL_expr `json:"PLpgSQL_expr"`
|
||||
}
|
||||
|
||||
// datum exists to match the expected JSON format.
|
||||
type datum struct {
|
||||
Record *plpgSQL_rec `json:"PLpgSQL_rec"`
|
||||
RecordField *plpgSQL_recfield `json:"PLpgSQL_recfield"`
|
||||
Row *plpgSQL_row `json:"PLpgSQL_row"`
|
||||
Variable *plpgSQL_var `json:"PLpgSQL_var"`
|
||||
}
|
||||
|
||||
// elsif exists to match the expected JSON format.
|
||||
type elsif struct {
|
||||
ElseIf plpgSQL_if_elsif `json:"PLpgSQL_if_elsif"`
|
||||
}
|
||||
|
||||
// expr exists to match the expected JSON format.
|
||||
type expr struct {
|
||||
Expression plpgSQL_expr `json:"PLpgSQL_expr"`
|
||||
}
|
||||
|
||||
// field exists to match the expected JSON format.
|
||||
type field struct {
|
||||
Name string `json:"name"`
|
||||
VariableNumber int32 `json:"varno"`
|
||||
}
|
||||
|
||||
// function exists to match the expected JSON format.
|
||||
type function struct {
|
||||
Function plpgSQL_block `json:"PLpgSQL_function"`
|
||||
}
|
||||
|
||||
// plpgSQL_block exists to match the expected JSON format.
|
||||
type plpgSQL_block struct {
|
||||
NewVariableNumber int32 `json:"new_varno"`
|
||||
OldVariableNumber int32 `json:"old_varno"`
|
||||
Datums []datum `json:"datums"`
|
||||
Action action `json:"action"`
|
||||
}
|
||||
|
||||
// plpgSQL_expr exists to match the expected JSON format.
|
||||
type plpgSQL_expr struct {
|
||||
Query string `json:"query"`
|
||||
ParseMode int32 `json:"parseMode"`
|
||||
}
|
||||
|
||||
// plpgSQL_if_elsif exists to match the expected JSON format.
|
||||
type plpgSQL_if_elsif struct {
|
||||
Condition cond `json:"cond"`
|
||||
Then []statement `json:"stmts"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_rec exists to match the expected JSON format.
|
||||
type plpgSQL_rec struct {
|
||||
RefName string `json:"refname"`
|
||||
DatumNumber int32 `json:"dno"`
|
||||
}
|
||||
|
||||
// plpgSQL_recfield exists to match the expected JSON format.
|
||||
type plpgSQL_recfield struct {
|
||||
FieldName string `json:"fieldname"`
|
||||
RecordParentNumber int32 `json:"recparentno"`
|
||||
}
|
||||
|
||||
// plpgSQL_row exists to match the expected JSON format.
|
||||
type plpgSQL_row struct {
|
||||
RefName string `json:"refname"`
|
||||
Fields []field `json:"fields"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_assign exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_assign struct {
|
||||
Expression expr `json:"expr"`
|
||||
VariableNumber int32 `json:"varno"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_block exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_block struct {
|
||||
Body []statement `json:"body"`
|
||||
Label string `json:"label"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_call exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_call struct {
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Expression expr `json:"expr"`
|
||||
IsCall bool `json:"is_call"`
|
||||
Target datum `json:"target"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_case exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_case struct {
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Expression expr `json:"t_expr"`
|
||||
// VarNo indicates the ID for the __Case__Variable_N__ variable that holds the evaluated
|
||||
// value of the case expression.
|
||||
VarNo int32 `json:"t_varno"`
|
||||
WhenList []statement `json:"case_when_list"`
|
||||
HasElse bool `json:"have_else"`
|
||||
Else []statement `json:"else_stmts"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_dynexecute exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_dynexecute struct {
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Into bool `json:"into"`
|
||||
Query expr `json:"query"`
|
||||
Target datum `json:"target"`
|
||||
Params []sqlstmt `json:"params"`
|
||||
}
|
||||
|
||||
// plpgSQL_case_when exists to match the expected JSON format.
|
||||
type plpgSQL_case_when struct {
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Expression expr `json:"expr"`
|
||||
Body []statement `json:"stmts"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_execsql exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_execsql struct {
|
||||
SQLStmt sqlstmt `json:"sqlstmt"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Into bool `json:"into"`
|
||||
Target datum `json:"target"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_exit exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_exit struct {
|
||||
Label string `json:"label"`
|
||||
IsExit bool `json:"is_exit"`
|
||||
Condition *expr `json:"cond"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_fori exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_fori struct {
|
||||
Label string `json:"label"`
|
||||
Var datum `json:"var"`
|
||||
Lower *expr `json:"lower"`
|
||||
Upper *expr `json:"upper"`
|
||||
Step *expr `json:"step"`
|
||||
Reverse bool `json:"reverse"`
|
||||
Body []statement `json:"body"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_fors exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_fors struct {
|
||||
Label string `json:"label"`
|
||||
Var datum `json:"var"`
|
||||
Body []statement `json:"body"`
|
||||
Query *expr `json:"query"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_if exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_if struct {
|
||||
Condition cond `json:"cond"`
|
||||
Then []statement `json:"then_body"`
|
||||
ElseIf []elsif `json:"elsif_list"`
|
||||
Else []statement `json:"else_body"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_loop exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_loop struct {
|
||||
Body []statement `json:"body"`
|
||||
Label string `json:"label"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_perform exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_perform struct {
|
||||
Expression expr `json:"expr"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_raise exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_raise struct {
|
||||
LineNumber int32 `json:"lineno"`
|
||||
ELogLevel int32 `json:"elog_level"`
|
||||
Message string `json:"message"`
|
||||
Params []sqlstmt `json:"params"`
|
||||
Options []plpgSQL_raise_option_wrapper `json:"options"`
|
||||
}
|
||||
|
||||
// plpgSQL_raise_option_wrapper exists to match the expected JSON format.
|
||||
type plpgSQL_raise_option_wrapper struct {
|
||||
Option plpgSQL_raise_option `json:"PLpgSQL_raise_option"`
|
||||
}
|
||||
|
||||
// plpgSQL_raise_option exists to match the expected JSON format.
|
||||
type plpgSQL_raise_option struct {
|
||||
OptionType int32 `json:"opt_type"`
|
||||
Expression sqlstmt `json:"expr"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_return exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_return struct {
|
||||
Expression expr `json:"expr"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_return_query exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_return_query struct {
|
||||
Query expr `json:"query"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_stmt_while exists to match the expected JSON format.
|
||||
type plpgSQL_stmt_while struct {
|
||||
Condition cond `json:"cond"`
|
||||
Body []statement `json:"body"`
|
||||
Label string `json:"label"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
}
|
||||
|
||||
// plpgSQL_type exists to match the expected JSON format.
|
||||
type plpgSQL_type struct {
|
||||
Name string `json:"typname"`
|
||||
}
|
||||
|
||||
// plpgSQL_var exists to match the expected JSON format.
|
||||
type plpgSQL_var struct {
|
||||
RefName string `json:"refname"`
|
||||
Type datatype `json:"datatype"`
|
||||
LineNumber int32 `json:"lineno"`
|
||||
Default default_val `json:"default_val"`
|
||||
}
|
||||
|
||||
// sqlstmt exists to match the expected JSON format.
|
||||
type sqlstmt struct {
|
||||
Expr plpgSQL_expr `json:"PLpgSQL_expr"`
|
||||
}
|
||||
|
||||
// statement exists to match the expected JSON format. Unlike other structs, this is used like a union rather than
|
||||
// having a singular expected implementation.
|
||||
type statement struct {
|
||||
Assignment *plpgSQL_stmt_assign `json:"PLpgSQL_stmt_assign"`
|
||||
Block *plpgSQL_stmt_block `json:"PLpgSQL_stmt_block"`
|
||||
Call *plpgSQL_stmt_call `json:"PLpgSQL_stmt_call"`
|
||||
Case *plpgSQL_stmt_case `json:"PLpgSQL_stmt_case"`
|
||||
DynExec *plpgSQL_stmt_dynexecute `json:"PLpgSQL_stmt_dynexecute"`
|
||||
ExecSQL *plpgSQL_stmt_execsql `json:"PLpgSQL_stmt_execsql"`
|
||||
Exit *plpgSQL_stmt_exit `json:"PLpgSQL_stmt_exit"`
|
||||
ForILoop *plpgSQL_stmt_fori `json:"PLpgSQL_stmt_fori"`
|
||||
ForSLoop *plpgSQL_stmt_fors `json:"PLpgSQL_stmt_fors"`
|
||||
If *plpgSQL_stmt_if `json:"PLpgSQL_stmt_if"`
|
||||
Loop *plpgSQL_stmt_loop `json:"PLpgSQL_stmt_loop"`
|
||||
Perform *plpgSQL_stmt_perform `json:"PLpgSQL_stmt_perform"`
|
||||
Raise *plpgSQL_stmt_raise `json:"PLpgSQL_stmt_raise"`
|
||||
Return *plpgSQL_stmt_return `json:"PLpgSQL_stmt_return"`
|
||||
ReturnQuery *plpgSQL_stmt_return_query `json:"PLpgSQL_stmt_return_query"`
|
||||
When *plpgSQL_case_when `json:"PLpgSQL_case_when"`
|
||||
While *plpgSQL_stmt_while `json:"PLpgSQL_stmt_while"`
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_assign) Convert() (Assignment, error) {
|
||||
query := stmt.Expression.Expression.Query
|
||||
varName := ""
|
||||
if equalsIdx := strings.Index(query, ":="); equalsIdx > 0 {
|
||||
varName = strings.TrimSpace(query[:equalsIdx])
|
||||
query = strings.TrimSpace(query[equalsIdx+2:])
|
||||
} else if equalsIdx = strings.Index(query, "="); equalsIdx > 0 {
|
||||
varName = strings.TrimSpace(query[:equalsIdx])
|
||||
query = strings.TrimSpace(query[equalsIdx+1:])
|
||||
} else {
|
||||
return Assignment{}, errors.New("PL/pgSQL assignment cannot find `:=` sign")
|
||||
}
|
||||
return Assignment{
|
||||
VariableName: varName,
|
||||
Expression: query,
|
||||
VariableIndex: stmt.VariableNumber,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_call) Convert() (ExecuteSQL, error) {
|
||||
var target string
|
||||
if !stmt.IsCall {
|
||||
if stmt.Target.Row != nil {
|
||||
names := make([]string, len(stmt.Target.Row.Fields))
|
||||
for i, rowField := range stmt.Target.Row.Fields {
|
||||
names[i] = rowField.Name
|
||||
}
|
||||
target = strings.Join(names, ",")
|
||||
} else if stmt.Target.Variable != nil {
|
||||
target = stmt.Target.Variable.RefName
|
||||
} else {
|
||||
return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
|
||||
}
|
||||
}
|
||||
return ExecuteSQL{
|
||||
Statement: stmt.Expression.Expression.Query,
|
||||
Target: target,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_case) Convert() (block Block, err error) {
|
||||
// If the CASE statement has a main expression, start by assigning it to a variable so
|
||||
// we can evaluate it once and only once.
|
||||
if stmt.Expression.Expression.Query != "" {
|
||||
// TODO: pg_query_go creates the definitions for these variables, and
|
||||
// ideally users shouldn't be able to reference them. We could
|
||||
// update all the references to them (i.e. declaration, assignment,
|
||||
// and WHEN block exprs) to change the name to include a \0 char to
|
||||
// prevent users from referencing them or colliding with them.
|
||||
block.Body = append(block.Body, Assignment{
|
||||
VariableName: fmt.Sprintf("__Case__Variable_%d__", stmt.VarNo),
|
||||
Expression: stmt.Expression.Expression.Query,
|
||||
})
|
||||
}
|
||||
|
||||
// Record indexes of all the GOTO ops that jump to the very end of the case block so we
|
||||
// can update them later and plug in the correct offsets after we know the final size.
|
||||
var gotoEndOpsIndexes []int
|
||||
|
||||
// Add operations for each WHEN statement...
|
||||
for _, stmt := range stmt.WhenList {
|
||||
when := stmt.When
|
||||
if when == nil {
|
||||
return Block{}, fmt.Errorf("case statement WHEN clause is nil")
|
||||
}
|
||||
|
||||
// TODO: The generated expressions from pg_query_go uses double quotes
|
||||
// around the variable name, which is valid for Postgres, but
|
||||
// our engine doesn't currently resolve double-quoted strings to
|
||||
// variables, so for now, we just extract the double quotes.
|
||||
expressionString := when.Expression.Expression.Query
|
||||
expressionString = strings.ReplaceAll(expressionString, `"`, "")
|
||||
|
||||
convertedWhenBodyStatements, err := jsonConvertStatements(when.Body)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
|
||||
block.Body = append(block.Body,
|
||||
If{
|
||||
Condition: expressionString,
|
||||
GotoOffset: 2,
|
||||
},
|
||||
Goto{
|
||||
// This GOTO jumps to the next WHEN block, so step over all the statements
|
||||
// from this WHEN block, plus 1 for the GOTO op we add at the end of each
|
||||
// block, and plus 1 more to move to the next statement.
|
||||
Offset: int32(len(convertedWhenBodyStatements) + 1 + 1),
|
||||
})
|
||||
block.Body = append(block.Body, convertedWhenBodyStatements...)
|
||||
|
||||
// Add a GOTO op to jump to the end of the entire CASE block, and record its position
|
||||
// in the statement block so we can update it later.
|
||||
block.Body = append(block.Body, Goto{})
|
||||
gotoEndOpsIndexes = append(gotoEndOpsIndexes, len(block.Body)-1)
|
||||
}
|
||||
|
||||
if stmt.HasElse {
|
||||
convertElseBodyStatements, err := jsonConvertStatements(stmt.Else)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
block.Body = append(block.Body, convertElseBodyStatements...)
|
||||
// TODO: If no cases match and there is no ELSE block, then add a RAISE statement
|
||||
// to return an error.
|
||||
//} else {
|
||||
// Sample PostgreSQL error response:
|
||||
// ERROR: case not found
|
||||
// HINT: CASE statement is missing ELSE part.
|
||||
// CONTEXT: PL/pgSQL function interpreted_case(integer) line 5 at CASE
|
||||
}
|
||||
|
||||
// Update all the GOTO ops that jump to the very end of the case block.
|
||||
for _, gotoEndOpIndex := range gotoEndOpsIndexes {
|
||||
// Sanity check that we are looking at a GOTO statement
|
||||
if _, ok := block.Body[gotoEndOpIndex].(Goto); !ok {
|
||||
return Block{}, fmt.Errorf("expected Goto statement, got %T", block.Body[gotoEndOpIndex])
|
||||
}
|
||||
|
||||
block.Body[gotoEndOpIndex] = Goto{
|
||||
Offset: int32(len(block.Body) - gotoEndOpIndex),
|
||||
}
|
||||
}
|
||||
|
||||
return block, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_dynexecute) Convert() (DynamicExecute, error) {
|
||||
var params []string
|
||||
for _, param := range stmt.Params {
|
||||
params = append(params, param.Expr.Query)
|
||||
}
|
||||
var target string
|
||||
if stmt.Into {
|
||||
switch {
|
||||
case stmt.Target.Row != nil:
|
||||
names := make([]string, len(stmt.Target.Row.Fields))
|
||||
for i, rowField := range stmt.Target.Row.Fields {
|
||||
names[i] = rowField.Name
|
||||
}
|
||||
target = strings.Join(names, ",")
|
||||
case stmt.Target.Variable != nil:
|
||||
target = stmt.Target.Variable.RefName
|
||||
default:
|
||||
return DynamicExecute{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
|
||||
}
|
||||
}
|
||||
query := strings.TrimSuffix(strings.TrimPrefix(stmt.Query.Expression.Query, "'"), "'")
|
||||
return DynamicExecute{
|
||||
Query: query,
|
||||
Params: params,
|
||||
Target: target,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_execsql) Convert() (ExecuteSQL, error) {
|
||||
var target string
|
||||
if stmt.Into {
|
||||
if stmt.Target.Row != nil {
|
||||
names := make([]string, len(stmt.Target.Row.Fields))
|
||||
for i, rowField := range stmt.Target.Row.Fields {
|
||||
names[i] = rowField.Name
|
||||
}
|
||||
target = strings.Join(names, ",")
|
||||
} else if stmt.Target.Variable != nil {
|
||||
target = stmt.Target.Variable.RefName
|
||||
} else {
|
||||
return ExecuteSQL{}, errors.Errorf("unhandled datum type: %T", stmt.Target)
|
||||
}
|
||||
}
|
||||
return ExecuteSQL{
|
||||
Statement: stmt.SQLStmt.Expr.Query,
|
||||
Target: target,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_exit) Convert() Statement {
|
||||
offset := int32(-1)
|
||||
if stmt.IsExit {
|
||||
offset = 1
|
||||
}
|
||||
var gotoStmt Goto
|
||||
if len(stmt.Label) > 0 {
|
||||
gotoStmt = Goto{
|
||||
Offset: offset,
|
||||
Label: stmt.Label,
|
||||
}
|
||||
} else {
|
||||
gotoStmt = Goto{
|
||||
Offset: offset,
|
||||
NearestScopeOp: true,
|
||||
}
|
||||
}
|
||||
if stmt.Condition == nil {
|
||||
return gotoStmt
|
||||
} else {
|
||||
return Block{
|
||||
Body: []Statement{
|
||||
If{
|
||||
Condition: stmt.Condition.Expression.Query,
|
||||
GotoOffset: 2,
|
||||
},
|
||||
Goto{Offset: 2},
|
||||
gotoStmt,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_fori) Convert() (block Block, err error) {
|
||||
block.Label = stmt.Label
|
||||
block.IsLoop = true
|
||||
|
||||
if stmt.Var.Variable == nil {
|
||||
return Block{}, errors.New("for loop variable cannot be nil")
|
||||
}
|
||||
varName := stmt.Var.Variable.RefName
|
||||
|
||||
// Extract bound and step expressions
|
||||
lowerExpr := "1"
|
||||
if stmt.Lower != nil {
|
||||
lowerExpr = stmt.Lower.Expression.Query
|
||||
}
|
||||
upperExpr := "1"
|
||||
if stmt.Upper != nil {
|
||||
upperExpr = stmt.Upper.Expression.Query
|
||||
}
|
||||
stepExpr := "1"
|
||||
if stmt.Step != nil {
|
||||
stepExpr = stmt.Step.Expression.Query
|
||||
}
|
||||
|
||||
// Determine init value, loop condition, and increment expression based on direction.
|
||||
// In the JSON, Lower is always the starting value and Upper is the ending bound.
|
||||
var condition, incrExpr string
|
||||
if stmt.Reverse {
|
||||
condition = fmt.Sprintf("%s >= (%s)", varName, upperExpr)
|
||||
incrExpr = fmt.Sprintf("%s - (%s)", varName, stepExpr)
|
||||
} else {
|
||||
condition = fmt.Sprintf("%s <= (%s)", varName, upperExpr)
|
||||
incrExpr = fmt.Sprintf("%s + (%s)", varName, stepExpr)
|
||||
}
|
||||
|
||||
// Convert the loop body.
|
||||
convertedBody, err := jsonConvertStatements(stmt.Body)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
bodySize := OperationSizeForStatements(convertedBody)
|
||||
|
||||
// Build the loop body:
|
||||
// [0] InitAssign: varName := lower
|
||||
// [1] If(condition, GotoOffset:2) → jumps to [3] (first body stmt) when true
|
||||
// [2] ExitGoto → offset=3+bodySize → jumps to ScopeEnd
|
||||
// [3..3+N-1] body statements (N = bodySize)
|
||||
// [3+N] IncrAssign: varName := varName +/- step
|
||||
// [3+N+1] BackGoto → offset=-(3+bodySize) → jumps back to If at [1]
|
||||
//
|
||||
// Because no variables are declared in this block (the loop variable is already
|
||||
// declared by the caller's DECLARE section), ScopeBegin is at M and the
|
||||
// InitAssign is at M+1, so all offsets are consistent.
|
||||
block.Body = []Statement{
|
||||
Assignment{
|
||||
VariableName: varName,
|
||||
Expression: lowerExpr,
|
||||
},
|
||||
If{
|
||||
Condition: condition,
|
||||
GotoOffset: 2,
|
||||
},
|
||||
Goto{
|
||||
Offset: 3 + bodySize,
|
||||
},
|
||||
}
|
||||
block.Body = append(block.Body, convertedBody...)
|
||||
block.Body = append(block.Body,
|
||||
Assignment{
|
||||
VariableName: varName,
|
||||
Expression: incrExpr,
|
||||
},
|
||||
Goto{
|
||||
Offset: -(3 + bodySize),
|
||||
},
|
||||
)
|
||||
return block, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_fors) Convert() (block Block, err error) {
|
||||
block.Label = stmt.Label
|
||||
block.IsLoop = true
|
||||
|
||||
if stmt.Query == nil {
|
||||
return Block{}, errors.New("FOR..IN..SELECT loop must have a query")
|
||||
}
|
||||
|
||||
var varName string
|
||||
switch {
|
||||
case stmt.Var.Record != nil:
|
||||
varName = stmt.Var.Record.RefName
|
||||
case stmt.Var.Variable != nil:
|
||||
varName = stmt.Var.Variable.RefName
|
||||
case stmt.Var.Row != nil:
|
||||
varName = stmt.Var.Row.RefName
|
||||
default:
|
||||
return Block{}, errors.New("FOR..IN..SELECT loop variable must be a record, row, or variable")
|
||||
}
|
||||
|
||||
// Use the line number to keep cursor names unique across multiple ForS loops
|
||||
// that might use the same variable name.
|
||||
cursorName := fmt.Sprintf("__cursor_%s_%d__", varName, stmt.LineNumber)
|
||||
query := stmt.Query.Expression.Query
|
||||
|
||||
convertedBody, err := jsonConvertStatements(stmt.Body)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
bodySize := OperationSizeForStatements(convertedBody)
|
||||
|
||||
// Layout inside the block (ScopeBegin/ScopeEnd are added by Block.AppendOperations):
|
||||
// [0] ForQueryInit – execute query, store rows in cursor
|
||||
// [1] ForQueryNext – fetch next row into varName, or jump forward by (bodySize+2) to ScopeEnd
|
||||
// [2..2+bodySize-1] body statements
|
||||
// [2+bodySize] Goto back to ForQueryNext: offset = -(1 + bodySize)
|
||||
block.Body = []Statement{
|
||||
ForQueryInit{CursorName: cursorName, Query: query},
|
||||
ForQueryNext{CursorName: cursorName, RecordVar: varName, GotoOffset: bodySize + 2},
|
||||
}
|
||||
block.Body = append(block.Body, convertedBody...)
|
||||
block.Body = append(block.Body, Goto{Offset: -(1 + bodySize)})
|
||||
return block, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_if) Convert() (Block, error) {
|
||||
// We store all GOTOs that will need to go to the end of the block. Since we can't know that ahead of time, we store
|
||||
// their indexes and set them at the end of the function.
|
||||
type gotoEndIndex struct {
|
||||
BodyIndex int
|
||||
GotoIndex int32
|
||||
}
|
||||
var gotoEndIndexes []gotoEndIndex
|
||||
returnBlock := Block{
|
||||
Body: []Statement{
|
||||
If{
|
||||
Condition: stmt.Condition.Expression.Query,
|
||||
GotoOffset: 2, // The operation following the conditional skips the THEN statements, so we're skipping that
|
||||
},
|
||||
},
|
||||
}
|
||||
// We'll parse our THEN statements, but we won't add them to the block just yet as we need their operation sizes
|
||||
thenStmts, err := jsonConvertStatements(stmt.Then)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
// When the condition is false, we want to skip our THEN block, so we do that (plus the GOTO which finishes the THEN block)
|
||||
returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(thenStmts) + 2})
|
||||
// Then we'll append our THEN block
|
||||
returnBlock.Body = append(returnBlock.Body, thenStmts...)
|
||||
// Then we want to append the GOTO that finishes the THEN block, but we don't know the end just yet, so we'll save
|
||||
// its index and fill it in later
|
||||
gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{
|
||||
BodyIndex: len(returnBlock.Body),
|
||||
GotoIndex: OperationSizeForStatements(returnBlock.Body),
|
||||
})
|
||||
returnBlock.Body = append(returnBlock.Body, Goto{})
|
||||
// We repeat the same process for each ELSIF statement (refer to the comments above)
|
||||
for _, elseIf := range stmt.ElseIf {
|
||||
returnBlock.Body = append(returnBlock.Body, If{
|
||||
Condition: elseIf.ElseIf.Condition.Expression.Query,
|
||||
GotoOffset: 2, // Same rules as skipping our THEN statement above
|
||||
})
|
||||
elseIfStmts, err := jsonConvertStatements(elseIf.ElseIf.Then)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
returnBlock.Body = append(returnBlock.Body, Goto{Offset: OperationSizeForStatements(elseIfStmts) + 2})
|
||||
returnBlock.Body = append(returnBlock.Body, elseIfStmts...)
|
||||
gotoEndIndexes = append(gotoEndIndexes, gotoEndIndex{
|
||||
BodyIndex: len(returnBlock.Body),
|
||||
GotoIndex: OperationSizeForStatements(returnBlock.Body),
|
||||
})
|
||||
returnBlock.Body = append(returnBlock.Body, Goto{})
|
||||
}
|
||||
// Finally we handle our ELSE statements. We don't have a condition to check, so we don't have to append any
|
||||
// additional GOTOs.
|
||||
elseStmts, err := jsonConvertStatements(stmt.Else)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
returnBlock.Body = append(returnBlock.Body, elseStmts...)
|
||||
// Now we'll set all of our GOTOs so that they skip to the end of the block.
|
||||
// We have to take their index position into account, since we want to skip to the end from their relative position.
|
||||
for _, idx := range gotoEndIndexes {
|
||||
returnBlock.Body[idx.BodyIndex] = Goto{Offset: OperationSizeForStatements(returnBlock.Body) - idx.GotoIndex}
|
||||
}
|
||||
return returnBlock, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_loop) Convert() (block Block, err error) {
|
||||
// Set the block's label if one was provided
|
||||
block.Label = stmt.Label
|
||||
block.IsLoop = true
|
||||
// Convert the body of the loop first so we can determine the GOTO offset
|
||||
block.Body, err = jsonConvertStatements(stmt.Body)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
// The loop returns to the beginning of the loop, skipping the body
|
||||
block.Body = append(block.Body, Goto{Offset: -OperationSizeForStatements(block.Body)})
|
||||
return block, nil
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_perform) Convert() Perform {
|
||||
return Perform{
|
||||
Statement: stmt.Expression.Expression.Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_raise) Convert() Raise {
|
||||
var params []string
|
||||
for _, param := range stmt.Params {
|
||||
params = append(params, param.Expr.Query)
|
||||
}
|
||||
|
||||
options := make(map[string]string)
|
||||
for _, option := range stmt.Options {
|
||||
options[strconv.Itoa(int(option.Option.OptionType))] = option.Option.Expression.Expr.Query
|
||||
}
|
||||
|
||||
return Raise{
|
||||
Level: NoticeLevel(uint8(stmt.ELogLevel)).String(),
|
||||
Message: stmt.Message,
|
||||
Params: params,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_return) Convert() Return {
|
||||
return Return{
|
||||
Expression: stmt.Expression.Expression.Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_return_query) Convert() ReturnQuery {
|
||||
return ReturnQuery{
|
||||
Query: stmt.Query.Expression.Query,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert converts the JSON statement into its output form.
|
||||
func (stmt *plpgSQL_stmt_while) Convert() (block Block, err error) {
|
||||
// Convert the body of the loop first so we can determine the GOTO offsets
|
||||
convertedLoopBodyStmts, err := jsonConvertStatements(stmt.Body)
|
||||
if err != nil {
|
||||
return Block{}, err
|
||||
}
|
||||
|
||||
block = Block{
|
||||
Body: []Statement{
|
||||
If{
|
||||
Condition: stmt.Condition.Expression.Query,
|
||||
// Jump forward two statements, so we skip over the GOTO below that exits the WHILE loop.
|
||||
GotoOffset: 2,
|
||||
},
|
||||
Goto{
|
||||
// Jump forward 1 statement to get to the loop body, then jump over the loop body and the
|
||||
// GOTO statement that jumps to the start of the WHILE loop.
|
||||
Offset: 1 + OperationSizeForStatements(convertedLoopBodyStmts) + 1,
|
||||
},
|
||||
},
|
||||
Label: stmt.Label,
|
||||
IsLoop: true,
|
||||
}
|
||||
|
||||
// Add the converted body of the WHILE loop, and a GOTO statement that jumps backwards past the current
|
||||
// GOTO statement, and past all the body statements, and past the GOTO statement at the start of the loop.
|
||||
block.Body = append(block.Body, convertedLoopBodyStmts...)
|
||||
block.Body = append(block.Body, Goto{Offset: -1 * (OperationSizeForStatements(convertedLoopBodyStmts) + 2)})
|
||||
return block, nil
|
||||
}
|
||||
Reference in New Issue
Block a user