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

238 lines
8.8 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"
"strings"
"github.com/cockroachdb/errors"
"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/planbuilder"
"github.com/dolthub/go-mysql-server/sql/transform"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/postgres/parser/parser"
"github.com/dolthub/doltgresql/postgres/parser/sem/tree"
"github.com/dolthub/doltgresql/server/ast"
"github.com/dolthub/doltgresql/server/expression"
"github.com/dolthub/doltgresql/server/node"
pgtransform "github.com/dolthub/doltgresql/server/transform"
pgtypes "github.com/dolthub/doltgresql/server/types"
)
// AddDomainConstraints adds domain type's default value and check constraints
// to the destination table schema and InsertNode/Update node's checks.
func AddDomainConstraints(ctx *sql.Context, a *analyzer.Analyzer, node sql.Node, scope *plan.Scope, selector analyzer.RuleSelector, qFlags *sql.QueryFlags) (sql.Node, transform.TreeIdentity, error) {
switch n := node.(type) {
case *plan.InsertInto:
return loadDomainConstraints(ctx, a, n, n.Schema(ctx))
case *plan.Update:
return loadDomainConstraints(ctx, a, n, n.Schema(ctx))
default:
return node, transform.SameTree, nil
}
}
// loadDomainConstraints retrieves and assigns domain type's default value, nullable and check constraints
// to the destination table schema and InsertNode/Update node's checks.
func loadDomainConstraints(ctx *sql.Context, a *analyzer.Analyzer, c sql.CheckConstraintNode, schema sql.Schema) (sql.Node, transform.TreeIdentity, error) {
// get current checks to append the domain checks to.
checks := c.Checks()
var same = transform.SameTree
for _, col := range schema {
if dt, ok := col.Type.(*pgtypes.DoltgresType); ok && dt.TypType == pgtypes.TypeType_Domain {
// assign column nullable
col.Nullable = !dt.NotNull
// get domain default value and assign to the column default value
defVal, err := getDomainDefault(ctx, a, dt.Default, col.Source, col.Type, col.Nullable)
if err != nil {
return nil, transform.SameTree, err
}
col.Default = defVal
// get domain checks
colChecks, err := getDomainCheckConstraintsForTable(ctx, a, col.Name, col.Source, dt.Checks)
if err != nil {
return nil, transform.SameTree, err
}
checks = append(checks, colChecks...)
same = transform.NewTree
}
}
if same == transform.SameTree {
return c, same, nil
}
return c.WithChecks(checks), same, nil
}
// getDomainDefault takes the default value definition, parses, builds and returns sql.ColumnDefaultValue.
func getDomainDefault(ctx *sql.Context, a *analyzer.Analyzer, defExpr, tblName string, typ sql.Type, nullable bool) (*sql.ColumnDefaultValue, error) {
if defExpr == "" {
return nil, nil
}
parsed, err := a.Parser.ParseSimple(fmt.Sprintf("select %s from %s", defExpr, tblName))
if err != nil {
return nil, err
}
selectStmt, ok := parsed.(*vitess.Select)
if !ok || len(selectStmt.SelectExprs) != 1 {
return nil, sql.ErrInvalidColumnDefaultValue.New(defExpr)
}
expr := selectStmt.SelectExprs[0]
ae, ok := expr.(*vitess.AliasedExpr)
if !ok {
return nil, sql.ErrInvalidColumnDefaultValue.New(defExpr)
}
builder := planbuilder.New(ctx, a.Catalog, nil)
return builder.BuildColumnDefaultValueWithTable(ae.Expr, selectStmt.From[0], typ, nullable), nil
}
// getDomainCheckConstraintsForTable takes the check constraint definitions, parses, builds and returns sql.CheckConstraints.
func getDomainCheckConstraintsForTable(ctx *sql.Context, a *analyzer.Analyzer, colName string, tblName string, checkDefs []*sql.CheckDefinition) (sql.CheckConstraints, error) {
checks := make(sql.CheckConstraints, len(checkDefs))
for i, check := range checkDefs {
q := fmt.Sprintf("select %s from %s", check.CheckExpression, tblName)
checkExpr, err := parseAndReplaceDomainCheckConstraint(ctx, a, check.CheckExpression, q, &tree.ColumnItem{
ColumnName: tree.Name(colName),
TableName: &tree.UnresolvedObjectName{NumParts: 1, Parts: [3]string{tblName}},
})
if err != nil {
return nil, err
}
checks[i] = &sql.CheckConstraint{
Name: check.Name,
Expr: checkExpr,
Enforced: true,
}
}
return checks, nil
}
// AddDomainConstraintsToCasts adds domain type's constraints to cast expressions.
func AddDomainConstraintsToCasts(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.NodeExprsWithOpaque(ctx, node, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
var same = transform.SameTree
switch e := expr.(type) {
case *expression.ExplicitCast:
if rt, ok := e.Type(ctx).(*pgtypes.DoltgresType); ok && rt.TypType == pgtypes.TypeType_Domain {
// the domain type should be resolved by this point
colChecks, err := getDomainCheckConstraintsForCast(ctx, a, rt.Checks, e.Child())
if err != nil {
return nil, transform.NewTree, err
}
same = transform.NewTree
expr = e.WithDomainConstraints(!rt.NotNull, colChecks)
}
return expr, same, nil
default:
// TODO: add ASSIGNMENT, IMPLICIT cast and other expressions that use domain types
return e, transform.SameTree, nil
}
})
}
// getDomainCheckConstraintsForCast takes the check constraint definitions, parses, builds and returns sql.CheckConstraints.
func getDomainCheckConstraintsForCast(ctx *sql.Context, a *analyzer.Analyzer, checkDefs []*sql.CheckDefinition, value sql.Expression) (sql.CheckConstraints, error) {
checks := make(sql.CheckConstraints, len(checkDefs))
for i, check := range checkDefs {
q := fmt.Sprintf("select %s", check.CheckExpression)
checkExpr, err := parseAndReplaceDomainCheckConstraint(ctx, a, check.CheckExpression, q, tree.DomainColumn{})
if err != nil {
return nil, err
}
// replace DomainColumn with given sql.Expression
checkExpr, _, _ = transform.Expr(ctx, checkExpr, func(ctx *sql.Context, expr sql.Expression) (sql.Expression, transform.TreeIdentity, error) {
switch e := expr.(type) {
case *node.DomainColumn:
expr = value
return expr, transform.NewTree, nil
default:
return e, transform.SameTree, nil
}
})
checks[i] = &sql.CheckConstraint{
Name: check.Name,
Expr: checkExpr,
Enforced: true,
}
}
return checks, nil
}
// parseAndReplaceDomainCheckConstraint parses check constraint and replaces the `VALUE` column
// reference with given Expr including resolved column given table and column names or DomainColumn.
// It returns built check expression.
func parseAndReplaceDomainCheckConstraint(ctx *sql.Context, a *analyzer.Analyzer, checkExpr, query string, replacesValue tree.Expr) (sql.Expression, error) {
stmt, err := parser.ParseOne(query)
if err != nil {
return nil, err
}
selectStmt, ok := stmt.AST.(*tree.Select)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
selectClause, ok := selectStmt.Select.(*tree.SelectClause)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
exprs := selectClause.Exprs
if len(exprs) != 1 {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
updatedCheckExpr, err := tree.SimpleVisit(exprs[0].Expr, func(visitingExpr tree.Expr) (recurse bool, newExpr tree.Expr, err error) {
switch v := visitingExpr.(type) {
case *tree.UnresolvedName:
if strings.ToLower(v.String()) != "value" {
return false, nil, errors.Errorf(`column "%s" does not exist`, v.String())
}
return false, replacesValue, nil
}
return true, visitingExpr, nil
})
if err != nil {
return nil, err
}
exprs[0].Expr = updatedCheckExpr
parsed, err := ast.Convert(stmt)
if err != nil {
return nil, err
}
convertedSelectStmt, ok := parsed.(*vitess.Select)
if !ok || len(convertedSelectStmt.SelectExprs) != 1 {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
expr := convertedSelectStmt.SelectExprs[0]
ae, ok := expr.(*vitess.AliasedExpr)
if !ok {
return nil, sql.ErrInvalidCheckConstraint.New(checkExpr)
}
builder := planbuilder.New(ctx, a.Catalog, nil)
var tblExpr vitess.TableExpr
if len(convertedSelectStmt.From) == 1 {
tblExpr = convertedSelectStmt.From[0]
}
return builder.BuildScalarWithTable(ae.Expr, tblExpr), nil
}