242 lines
7.0 KiB
Go
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
|
|
}
|