Files
dolthub--doltgresql/server/compare/utils.go
T
2026-07-13 12:32:25 +08:00

145 lines
5.2 KiB
Go

// 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 compare
import (
"fmt"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/expression"
"github.com/dolthub/doltgresql/server/functions/framework"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// CompareRecords compares two record values using the specified comparison operator, |op| and returns a result
// indicating if the comparison was true, false, or indeterminate (nil).
//
// More info on rules for comparing records:
// https://www.postgresql.org/docs/current/functions-comparisons.html#ROW-WISE-COMPARISON
func CompareRecords(ctx *sql.Context, op framework.Operator, v1 interface{}, v2 interface{}) (res any, err error) {
leftRecord, rightRecord, err := checkRecordArgs(v1, v2)
if err != nil {
return nil, err
}
// TODO: This can be a hot path when filtering a large table against a tuple, for example.
// We can reduce branching by splitting each case into their individual functions, which will also make the code
// more readable. A downside would be a ton of repeated code.
var hasNull bool
var leftLiteral, rightLiteral expression.Literal
for i := 0; i < len(leftRecord); i++ {
// NULL values are by definition not comparable
if leftRecord[i].Value == nil || rightRecord[i].Value == nil {
switch op {
case framework.Operator_BinaryEqual, framework.Operator_BinaryNotEqual:
hasNull = true
continue
default:
return nil, nil
}
}
leftLiteral.Val = leftRecord[i].Value
leftLiteral.Typ = leftRecord[i].Type
rightLiteral.Val = rightRecord[i].Value
rightLiteral.Typ = rightRecord[i].Type
switch op {
case framework.Operator_BinaryEqual:
res, err = callComparisonFunction(ctx, framework.Operator_BinaryEqual, &leftLiteral, &rightLiteral)
if err != nil {
return false, err
}
if res == false {
return false, nil
}
case framework.Operator_BinaryNotEqual:
res, err = callComparisonFunction(ctx, framework.Operator_BinaryNotEqual, &leftLiteral, &rightLiteral)
if err != nil {
return false, err
}
if res == true {
return true, nil
}
// Records are compared by evaluating each field in order of significance, so if the fields are equal, we need
// to compare the next field.
case framework.Operator_BinaryLessThan, framework.Operator_BinaryLessOrEqual,
framework.Operator_BinaryGreaterThan, framework.Operator_BinaryGreaterOrEqual:
switch op {
case framework.Operator_BinaryLessThan, framework.Operator_BinaryLessOrEqual:
res, err = callComparisonFunction(ctx, framework.Operator_BinaryLessThan, &leftLiteral, &rightLiteral)
default:
res, err = callComparisonFunction(ctx, framework.Operator_BinaryGreaterThan, &leftLiteral, &rightLiteral)
}
if err != nil {
return false, err
}
if res == true {
return true, nil
}
// If equals fields are equal, move onto the next field, else return false
res, err = callComparisonFunction(ctx, framework.Operator_BinaryEqual, &leftLiteral, &rightLiteral)
if err != nil {
return false, err
}
if res == false {
return false, nil
}
default:
return false, fmt.Errorf("unsupported binary operator: %s", op)
}
}
if hasNull {
return nil, nil
}
// Every field is equal
switch op {
case framework.Operator_BinaryEqual, framework.Operator_BinaryLessOrEqual, framework.Operator_BinaryGreaterOrEqual:
return true, nil
default:
return false, nil
}
}
// checkRecordArgs asserts that |v1| and |v2| are both []pgtypes.RecordValue, and that they have the same number of
// elements, then returns them. If any problems were detected, an error is returned instead.
func checkRecordArgs(v1, v2 interface{}) (leftRecord, rightRecord []pgtypes.RecordValue, err error) {
var ok bool
leftRecord, ok = v1.([]pgtypes.RecordValue)
if !ok {
return nil, nil, fmt.Errorf("expected a RecordValue, but got %T", v1)
}
rightRecord, ok = v2.([]pgtypes.RecordValue)
if !ok {
return nil, nil, fmt.Errorf("expected a RecordValue, but got %T", v2)
}
if len(leftRecord) != len(rightRecord) {
return nil, nil, fmt.Errorf("unequal number of entries in row expressions")
}
return leftRecord, rightRecord, nil
}
// callComparisonFunction invokes the binary comparison function for the specified operator |op| with the two arguments
// |leftLiteral| and |rightLiteral|. The result and any error are returned.
func callComparisonFunction(ctx *sql.Context, op framework.Operator, leftLiteral, rightLiteral sql.Expression) (result any, err error) {
intermediateFunction := framework.GetBinaryFunction(op)
compiledFunction := intermediateFunction.Compile(
ctx, "_internal_record_comparison_function", leftLiteral, rightLiteral)
return compiledFunction.Eval(ctx, nil)
}