Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

thema: Initial pass at Go lenses/migrations #187

Merged
merged 8 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 120 additions & 2 deletions bind.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package thema

import (
"bytes"
"fmt"

"cuelang.org/go/cue"
cerrors "cuelang.org/go/cue/errors"
"cuelang.org/go/cue/token"
"github.com/cockroachdb/errors"

terrors "github.com/grafana/thema/errors"
"github.com/grafana/thema/internal/compat"
)
Expand Down Expand Up @@ -39,10 +41,27 @@ type maybeLineage struct {

allv []SyntacticVersion

implens []ImperativeLens

lensmap map[lensID]ImperativeLens

// The raw input value is the root of a package instance
// rawIsPackage bool
}

// to, from
type lensID struct {
From, To SyntacticVersion
}

func lid(from, to SyntacticVersion) lensID {
return lensID{from, to}
}

func (id lensID) String() string {
return fmt.Sprintf("%s -> %s", id.From, id.To)
}

func (ml *maybeLineage) checkGoValidity(cfg *bindConfig) error {
schiter, err := ml.uni.LookupPath(cue.MakePath(cue.Str("schemas"))).List()
if err != nil {
Expand Down Expand Up @@ -167,6 +186,12 @@ func (ml *maybeLineage) checkNativeValidity(cfg *bindConfig) error {
}

func (ml *maybeLineage) checkLensesOrder() error {
// Two distinct validation paths, depending on whether the lenses were defined in
// Go or CUE.
if len(ml.implens) > 0 {
return ml.checkGoLensCompleteness()
}

lensIter, err := ml.uni.LookupPath(cue.MakePath(cue.Str("lenses"))).List()
if err != nil {
return nil // no lenses found
Expand All @@ -179,7 +204,7 @@ func (ml *maybeLineage) checkLensesOrder() error {
return err
}

if err := checkLensesOrder(previous, &curr); err != nil {
if err := doCheck(previous, &curr); err != nil {
return err
}

Expand All @@ -189,6 +214,99 @@ func (ml *maybeLineage) checkLensesOrder() error {
return nil
}

func (ml *maybeLineage) checkGoLensCompleteness() error {
// TODO(sdboyer) it'd be nice to consolidate all the errors so that the user always sees a complete set of problems
all := make(map[lensID]bool)
for _, lens := range ml.implens {
id := lid(lens.From, lens.To)
if all[id] {
return fmt.Errorf("duplicate Go migration %s", id)
}
if lens.Mapper == nil {
return fmt.Errorf("nil Go migration func for %s", id)
}
all[id] = true
}

var missing []lensID

var prior SyntacticVersion
for _, sch := range ml.schlist[1:] {
// there must always at least be a reverse lens
v := sch.Version()
revid := lid(v, prior)

if !all[revid] {
missing = append(missing, revid)
} else {
delete(all, revid)
}

if v[0] != prior[0] {
// if we crossed a major version, there must also be a forward lens
fwdid := lid(prior, v)
if !all[fwdid] {
missing = append(missing, fwdid)
} else {
delete(all, fwdid)
}
}
prior = v
}

// TODO is it worth making each sub-item into its own error type?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don't see any need (at least for now), to explicitly model every item into its own error, as far as we bring that information back to the consumer.

if len(missing) > 0 {
b := new(bytes.Buffer)

fmt.Fprintf(b, "Go migrations not provided for the following version pairs:\n")
for _, mlid := range missing {
fmt.Fprint(b, "\t", mlid, "\n")
}
return errors.Mark(errors.New(b.String()), terrors.ErrMissingLenses)
}

if len(all) > 0 {
b := new(bytes.Buffer)

fmt.Fprintf(b, "Go migrations erroneously provided for the following version pairs:\n")
// walk the slice so output is reliably ordered
for _, lens := range ml.implens {
// if it's not in the list it's because it was expected & already processed
elid := lid(lens.From, lens.To)
if _, has := all[elid]; !has {
continue
}
if !synvExists(ml.allv, lens.To) {
fmt.Fprintf(b, "\t%s (schema version %s does not exist)", elid, lens.To)
} else if !synvExists(ml.allv, lens.From) {
fmt.Fprintf(b, "\t%s (schema version %s does not exist)", elid, lens.From)
} else if elid.To == elid.From {
fmt.Fprintf(b, "\t%s (self-migrations not allowed)", elid)
} else if elid.To.Less(elid.From) {
// reverse lenses
// only possibility is non-sequential versions connected
fmt.Fprintf(b, "\t%s (%s is predecessor of %s, not %s)", elid, ml.allv[searchSynv(ml.allv, elid.From)-1], elid.From, elid.To)
} else {
// forward lenses
// either a minor lens was provided, or non-sequential versions connected
if lens.To[0] != lens.From[0] {
fmt.Fprintf(b, "\t%s (minor version upgrades are handled automatically)", elid)
} else {
fmt.Fprintf(b, "\t%s (%s is successor of %s, not %s)", elid, ml.allv[searchSynv(ml.allv, elid.From)+1], elid.From, elid.To)
}
}
}
return errors.Mark(errors.New(b.String()), terrors.ErrErroneousLenses)
}

ml.lensmap = make(map[lensID]ImperativeLens, len(ml.implens))
for _, lens := range ml.implens {
ml.lensmap[lid(lens.From, lens.To)] = lens
}

return nil
}

type lensVersionDef struct {
to SyntacticVersion
from SyntacticVersion
Expand All @@ -209,7 +327,7 @@ func newLensVersionDef(val cue.Value) (lensVersionDef, error) {
return lensVersionDef{to: to, from: from}, err
}

func checkLensesOrder(prev, curr *lensVersionDef) error {
func doCheck(prev, curr *lensVersionDef) error {
if prev == nil {
return nil
}
Expand Down
14 changes: 14 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ var (
// ErrInvalidLensesOrder indicates that lenses are in the wrong order - they must be sorted by `to`, then `from`.
ErrInvalidLensesOrder = errors.New("lenses in lineage are not ordered by version")

// ErrDuplicateLenses indicates that a lens was defined declaratively in CUE, but the same lens
// was also provided as a Go function to BindLineage.
ErrDuplicateLenses = errors.New("lens is declared in both CUE and Go")

// ErrMissingLenses indicates that the lenses provided to BindLineage in either
// CUE or Go were missing at least one of the expected lenses determined by the
// set of schemas in the lineage.
ErrMissingLenses = errors.New("not all expected lenses were provided")

// ErrErroneousLenses indicates that a lens was provided to BindLineage in either
// CUE or Go that was not one of the expected lenses determined by the set of
// schemas in the lineage.
ErrErroneousLenses = errors.New("unexpected lenses were erroneously provided")

// ErrVersionNotExist indicates that no schema exists in a lineage with a
// given version.
ErrVersionNotExist = errors.New("lineage does not contain schema with version") // ErrNoSchemaWithVersion
Expand Down
Loading