Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

253 lines
6.9 KiB
Go

// Copyright 2019 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 actions
import (
"fmt"
"io"
"strings"
gms "github.com/dolthub/go-mysql-server"
"github.com/dolthub/go-mysql-server/sql"
goerrors "gopkg.in/src-d/go-errors.v1"
"github.com/dolthub/dolt/go/libraries/doltcore/diff"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/store/datas"
)
const CommitVerificationFailedPrefix = "commit verification failed:"
var ErrCommitVerificationFailed = goerrors.NewKind(CommitVerificationFailedPrefix + " %s")
// CommitStagedProps contains the parameters for a staged commit operation.
type CommitStagedProps struct {
Message string
AllowEmpty bool
SkipEmpty bool
Amend bool
Force bool
SkipVerification bool
// Author is the identity of the person who wrote the change.
Author datas.CommitIdent
// Committer is the identity of the person who applied the change.
Committer datas.CommitIdent
}
const (
// System variable name, defined here to avoid circular imports
DoltCommitVerificationGroups = "dolt_commit_verification_groups"
)
// getCommitRunTestGroups returns the test groups to run for commit operations
// Returns empty slice if no tests should be run, ["*"] if all tests should be run,
// or specific group names if only those groups should be run
func getCommitRunTestGroups() []string {
_, val, ok := sql.SystemVariables.GetGlobal(DoltCommitVerificationGroups)
if !ok {
return nil
}
if stringVal, ok := val.(string); ok && stringVal != "" {
if stringVal == "*" {
return []string{"*"}
}
// Split by comma and trim whitespace
groups := strings.Split(stringVal, ",")
for i, group := range groups {
groups[i] = strings.TrimSpace(group)
}
return groups
}
return nil
}
// GetCommitStaged returns a new pending commit with the roots and commit properties given.
func GetCommitStaged(
ctx *sql.Context,
tableResolver doltdb.TableResolver,
roots doltdb.Roots,
ws *doltdb.WorkingSet,
mergeParents []*doltdb.Commit,
db *doltdb.DoltDB,
props CommitStagedProps,
) (*doltdb.PendingCommit, error) {
if props.Message == "" {
return nil, datas.ErrEmptyCommitMessage
}
allowEmpty := ws.MergeActive() || props.AllowEmpty || props.Amend
// HashOf returns a cached value, so comparing the staged root against HEAD
// here avoids the full table delta walk for callers with nothing staged.
if !allowEmpty {
stagedHash, err := roots.Staged.HashOf()
if err != nil {
return nil, err
}
headHash, err := roots.Head.HashOf()
if err != nil {
return nil, err
}
if stagedHash == headHash {
if props.SkipEmpty {
return nil, nil
}
return nil, NothingStaged{}
}
}
stagedTables, notStaged, err := diff.GetStagedUnstagedTableDeltas(ctx, roots)
if err != nil {
return nil, err
}
var stagedTblNames []doltdb.TableName
for _, td := range stagedTables {
n := td.ToName
if td.IsDrop() {
n = td.FromName
}
stagedTblNames = append(stagedTblNames, n)
}
stagedSchemas, _, err := diff.GetStagedUnstagedDatabaseSchemaDeltas(ctx, roots)
if err != nil {
return nil, err
}
isEmpty := len(stagedTables) == 0 && len(stagedSchemas) == 0
if isEmpty && props.SkipEmpty {
return nil, nil
}
if isEmpty && !allowEmpty {
return nil, NothingStaged{notStaged}
}
if !props.Force {
inConflict, err := doltdb.TablesWithDataConflicts(ctx, roots.Working)
if err != nil {
return nil, err
}
if len(inConflict) > 0 {
return nil, NewTblInConflictError(inConflict)
}
violatesConstraints, err := doltdb.TablesWithConstraintViolations(ctx, roots.Working)
if err != nil {
return nil, err
}
if len(violatesConstraints) > 0 {
return nil, NewTblHasConstraintViolations(violatesConstraints)
}
if ws.MergeActive() {
schConflicts := ws.MergeState().TablesWithSchemaConflicts()
if len(schConflicts) > 0 {
return nil, NewTblSchemaConflictError(schConflicts)
}
}
fkChecks, err := ctx.GetSessionVariable(ctx, "foreign_key_checks")
if err != nil {
return nil, err
}
if intValue, ok := fkChecks.(int8); ok && intValue == 1 {
roots.Staged, err = doltdb.ValidateForeignKeysOnSchemas(ctx, tableResolver, roots.Staged)
if err != nil {
return nil, err
}
}
}
if !props.SkipVerification {
testGroups := getCommitRunTestGroups()
if len(testGroups) > 0 {
err := runCommitVerification(ctx, testGroups)
if err != nil {
return nil, err
}
}
}
commitMeta, err := datas.NewCommitMetaWithAuthorCommitter(props.Author, props.Committer, props.Message)
if err != nil {
return nil, err
}
return db.NewPendingCommit(ctx, roots, mergeParents, props.Amend, commitMeta)
}
// runCommitVerification runs the commit verification tests for the given test groups.
// If any tests fail, it returns ErrCommitVerificationFailed wrapping the failure details.
// Callers can use errors.Is(err, ErrCommitVerificationFailed) to detect this case.
func runCommitVerification(ctx *sql.Context, testGroups []string) error {
type sessionInterface interface {
sql.Session
GenericProvider() sql.MutableDatabaseProvider
}
session, ok := ctx.Session.(sessionInterface)
if !ok {
return fmt.Errorf("session does not provide database provider interface")
}
provider := session.GenericProvider()
engine := gms.NewDefault(provider)
return runTestsUsingDtablefunctions(ctx, engine, testGroups)
}
// runTestsUsingDtablefunctions runs tests using the dtablefunctions package against the staged root
func runTestsUsingDtablefunctions(ctx *sql.Context, engine *gms.Engine, testGroups []string) error {
if len(testGroups) == 0 {
return nil
}
var allFailures []string
for _, group := range testGroups {
query := fmt.Sprintf("SELECT * FROM dolt_test_run('%s')", group)
_, iter, _, err := engine.Query(ctx, query)
if err != nil {
return fmt.Errorf("failed to run dolt_test_run for group %s: %w", group, err)
}
for {
row, rErr := iter.Next(ctx)
if rErr == io.EOF {
break
}
if rErr != nil {
return fmt.Errorf("error reading test results: %w", rErr)
}
// Extract status (column 3)
status := fmt.Sprintf("%v", row[3])
if status != "PASS" {
testName := fmt.Sprintf("%v", row[0])
message := fmt.Sprintf("%v", row[4])
allFailures = append(allFailures, fmt.Sprintf("%s (%s)", testName, message))
}
}
}
if len(allFailures) > 0 {
return ErrCommitVerificationFailed.New(strings.Join(allFailures, ", "))
}
return nil
}