func checkSchemaVersionsHaveNoHoles(versions []int) error {
	for i := range versions[:len(versions)-1] {
		if versions[i+1] != versions[i]+1 {
			return errors.Errorf("missing updates: %d -> %d", versions[i], versions[i+1])
		}
	}
	return nil
}
Check that the given list of update version numbers doesn't have "holes",
that is each version equal the preceding version plus 1.
package schema
import (
	"fmt"
	"strings"
	"github.com/pkg/errors"
	"github.com/spoke-d/thermionic/internal/db/database"
	"github.com/spoke-d/thermionic/internal/db/query"
	"github.com/spoke-d/thermionic/internal/fsys"
)
type Schema struct {
	fileSystem fsys.FileSystem
	updates    []Update  
	hook       Hook      
	fresh      string    
	check      Check     
	path       string    
}
type Update func(database.Tx) error
type Hook func(int, database.Tx) error
type Check func(int, database.Tx) error
func New(fileSystem fsys.FileSystem, updates []Update) *Schema {
	return &Schema{
		fileSystem: fileSystem,
		updates:    updates,
	}
}
func Empty(fileSystem fsys.FileSystem) *Schema {
	return New(fileSystem, make([]Update, 0))
}
func (s *Schema) Add(update Update) {
	s.updates = append(s.updates, update)
}
func (s *Schema) Len() int {
	return len(s.updates)
}
func (s *Schema) Hook(hook Hook) {
	s.hook = hook
}
func (s *Schema) Check(check Check) {
	s.check = check
}
func (s *Schema) Fresh(statement string) {
	s.fresh = statement
}
func (s *Schema) File(path string) {
	s.path = path
}
func (s *Schema) Trim(version int) []Update {
	trimmed := s.updates[version:]
	s.updates = s.updates[:version]
	s.fresh = ""
	return trimmed
}
func (s *Schema) Ensure(src database.DB) (int, error) {
	var (
		current int
		aborted bool
	)
	err := query.Transaction(src, func(tx database.Tx) error {
		if err := execFromFile(s.fileSystem, tx, s.path, s.hook); err != nil {
			return errors.Wrapf(err, "failed to execute queries from %q", s.path)
		}
		if err := ensureSchemaTableExists(tx); err != nil {
			return errors.WithStack(err)
		}
		var err error
		current, err = queryCurrentVersion(tx)
		if err != nil {
			return errors.WithStack(err)
		}
		if s.check != nil {
			if err := s.check(current, tx); err == ErrGracefulAbort {
				aborted = true
				return nil
			} else if err != nil {
				return errors.WithStack(err)
			}
		}
		if current == 0 && s.fresh != "" {
			if _, err := tx.Exec(s.fresh); err != nil {
				return errors.Wrap(err, "cannot apply fresh schema")
			}
		} else {
			err = ensureUpdatesAreApplied(tx, current, s.updates, s.hook)
			return errors.WithStack(err)
		}
		return nil
	})
	if err != nil {
		return -1, errors.WithStack(err)
	}
	if aborted {
		return current, ErrGracefulAbort
	}
	return current, nil
}
func (s *Schema) Dump(src database.DB) (string, error) {
	var statements []string
	if err := query.Transaction(src, func(tx database.Tx) error {
		err := checkAllUpdatesAreApplied(tx, s.updates)
		if err != nil {
			return errors.WithStack(err)
		}
		statements, err = selectTablesSQL(tx)
		return errors.WithStack(err)
	}); err != nil {
		return "", errors.WithStack(err)
	}
	for i, statement := range statements {
		statements[i] = formatSQL(statement)
	}
	statements = append(
		statements,
		fmt.Sprintf(StmtDump, len(s.updates)))
	return strings.Join(statements, ";\n"), nil
}
func ensureSchemaTableExists(tx database.Tx) error {
	exists, err := SchemaTableExists(tx)
	if err != nil {
		return errors.Wrap(err, "failed to check if schema table is there")
	}
	if !exists {
		if err := createSchemaTable(tx); err != nil {
			return errors.Wrap(err, "failed to create schema table")
		}
	}
	return nil
}
func queryCurrentVersion(tx database.Tx) (int, error) {
	versions, err := selectSchemaVersions(tx)
	if err != nil {
		return -1, errors.Wrap(err, "failed to fetch update versions")
	}
	var current int
	if len(versions) > 0 {
		if err := checkSchemaVersionsHaveNoHoles(versions); err != nil {
			return -1, errors.WithStack(err)
		}
		current = versions[len(versions)-1]  
	}
	return current, nil
}