253 lines
6.9 KiB
Go
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
|
|
}
|