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

242 lines
7.0 KiB
Go

// Copyright 2026 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 node
import (
"context"
"io"
"strings"
"github.com/cockroachdb/errors"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle/expranalysis"
"github.com/dolthub/go-mysql-server/sql"
"github.com/dolthub/go-mysql-server/sql/plan"
vitess "github.com/dolthub/vitess/go/vt/sqlparser"
"github.com/dolthub/doltgresql/core"
)
// ValidateConstraint handles the ALTER TABLE ... VALIDATE CONSTRAINT statement.
type ValidateConstraint struct {
DbProvider sql.DatabaseProvider
schemaName string
tableName string
constraintName string
}
var _ sql.ExecSourceRel = (*ValidateConstraint)(nil)
var _ sql.MultiDatabaser = (*ValidateConstraint)(nil)
var _ vitess.Injectable = (*ValidateConstraint)(nil)
// NewValidateConstraint returns a new *ValidateConstraint.
func NewValidateConstraint(schemaName, tableName, constraintName string) *ValidateConstraint {
return &ValidateConstraint{
schemaName: schemaName,
tableName: tableName,
constraintName: constraintName,
}
}
// Children implements sql.ExecSourceRel.
func (v *ValidateConstraint) Children() []sql.Node { return nil }
// IsReadOnly implements sql.ExecSourceRel.
func (v *ValidateConstraint) IsReadOnly() bool { return false }
// Resolved implements sql.ExecSourceRel.
func (v *ValidateConstraint) Resolved() bool {
return v.DbProvider != nil
}
// Schema implements sql.ExecSourceRel.
func (v *ValidateConstraint) Schema(ctx *sql.Context) sql.Schema { return nil }
// String implements sql.ExecSourceRel.
func (v *ValidateConstraint) String() string { return "VALIDATE CONSTRAINT" }
// WithChildren implements sql.ExecSourceRel.
func (v *ValidateConstraint) WithChildren(ctx *sql.Context, children ...sql.Node) (sql.Node, error) {
return plan.NillaryWithChildren(v, children...)
}
// WithResolvedChildren implements vitess.Injectable.
func (v *ValidateConstraint) WithResolvedChildren(ctx context.Context, children []any) (any, error) {
if len(children) != 0 {
return nil, ErrVitessChildCount.New(0, len(children))
}
return v, nil
}
// DatabaseProvider implements sql.MultiDatabaser.
func (v *ValidateConstraint) DatabaseProvider() sql.DatabaseProvider { return v.DbProvider }
// WithDatabaseProvider implements sql.MultiDatabaser.
func (v *ValidateConstraint) WithDatabaseProvider(provider sql.DatabaseProvider) (sql.Node, error) {
nv := *v
nv.DbProvider = provider
return &nv, nil
}
// RowIter implements sql.ExecSourceRel.
func (v *ValidateConstraint) RowIter(ctx *sql.Context, r sql.Row) (sql.RowIter, error) {
db, err := v.DbProvider.Database(ctx, ctx.GetCurrentDatabase())
if err != nil {
return nil, err
}
schemaName := v.schemaName
if schemaName == "" {
schemaName, err = core.GetCurrentSchema(ctx)
if err != nil {
return nil, err
}
}
schemaDb, ok := db.(sql.SchemaDatabase)
if !ok {
return nil, errors.Errorf("database does not support schemas")
}
dbSchema, ok, err := schemaDb.GetSchema(ctx, schemaName)
if err != nil {
return nil, err
}
if !ok {
return nil, errors.Errorf("schema %s does not exist", schemaName)
}
tblNode, _, err := dbSchema.GetTableInsensitive(ctx, v.tableName)
if err != nil {
return nil, err
}
if tblNode == nil {
return nil, sql.ErrTableNotFound.New(v.tableName)
}
// validate foreign key constraint
if fkTbl, ok := tblNode.(sql.ForeignKeyTable); ok {
fks, err := fkTbl.GetDeclaredForeignKeys(ctx)
if err != nil {
return nil, err
}
for _, fk := range fks {
if strings.EqualFold(fk.Name, v.constraintName) {
return v.validateForeignKey(ctx, db, fkTbl, fk)
}
}
}
// validate check constraint
if checkTbl, ok := tblNode.(sql.CheckTable); ok {
checks, err := checkTbl.GetChecks(ctx)
if err != nil {
return nil, err
}
for _, check := range checks {
if strings.EqualFold(check.Name, v.constraintName) {
return v.validateCheckConstraint(ctx, tblNode, check)
}
}
}
return nil, errors.Errorf(`constraint "%s" of relation "%s" does not exist`, v.constraintName, v.tableName)
}
// validateCheckConstraint validates check constraints if on its |IsNotValid| field is TRUE.
func (v *ValidateConstraint) validateCheckConstraint(ctx *sql.Context, tblNode sql.Table, check sql.CheckDefinition) (sql.RowIter, error) {
if !check.IsNotValid {
return sql.RowsToRowIter(), nil
}
checkExpr, err := expranalysis.ResolveExpression(ctx, v.tableName, check.CheckExpression)
if err != nil {
return nil, errors.Errorf("could not parse check expression for constraint %q: %v", check.Name, err)
}
partitions, err := tblNode.Partitions(ctx)
if err != nil {
return nil, err
}
defer partitions.Close(ctx)
for {
partition, err := partitions.Next(ctx)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
rows, err := tblNode.PartitionRows(ctx, partition)
if err != nil {
return nil, err
}
defer rows.Close(ctx)
for {
row, err := rows.Next(ctx)
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
res, err := sql.EvaluateCondition(ctx, checkExpr, row)
if err != nil {
return nil, err
}
if sql.IsFalse(res) {
return nil, sql.ErrCheckConstraintViolated.New(check.Name)
}
}
}
// TODO: clear the IsNotValid flag so pg_constraint reports convalidated = true
return sql.RowsToRowIter(), nil
}
// validateForeignKey validates foreign key constraints if on its |IsNotValid| field is TRUE.
func (v *ValidateConstraint) validateForeignKey(ctx *sql.Context, db sql.Database, fkTbl sql.ForeignKeyTable, fkDef sql.ForeignKeyConstraint) (sql.RowIter, error) {
if !fkDef.IsNotValid {
return sql.RowsToRowIter(), nil
}
refTblNode, _, err := db.GetTableInsensitive(ctx, fkDef.ParentTable)
if err != nil {
return nil, err
}
if refTblNode == nil {
return nil, sql.ErrTableNotFound.New(fkDef.ParentTable)
}
refFkTbl, ok := refTblNode.(sql.ForeignKeyTable)
if !ok {
return nil, errors.Errorf("table %s does not support foreign key constraints", fkDef.ParentTable)
}
// this is to allow calling plan.ResolveForeignKey function to validate
fkDef.IsResolved = false
if err = plan.ResolveForeignKey(ctx, fkTbl, refFkTbl, fkDef, false, true, true); err != nil {
// TODO: fix - currently error message includes "cannot add or update a child row"
return nil, err
}
// undo - it's not used but safe to set it
fkDef.IsResolved = true
// clear the IsNotValid flag so pg_constraint reports convalidated = true
fkDef.IsNotValid = false
if err = fkTbl.UpdateForeignKey(ctx, fkDef.Name, fkDef); err != nil {
return nil, err
}
return sql.RowsToRowIter(), nil
}