Repository: schemahero/schemahero
Pull request #880 (2023-02-20 22:43:07)

Sort specs so tables are processed before views


Patch:
diff --git a/pkg/cli/schemaherokubectlcli/plan.go b/pkg/cli/schemaherokubectlcli/plan.go
index 19a09048..06e4d295 100644
--- a/pkg/cli/schemaherokubectlcli/plan.go
+++ b/pkg/cli/schemaherokubectlcli/plan.go
@@ -2,11 +2,13 @@ package schemaherokubectlcli
 
 import (
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 
 	"github.com/pkg/errors"
 	"github.com/schemahero/schemahero/pkg/database"
+	"github.com/schemahero/schemahero/pkg/database/types"
 	"github.com/schemahero/schemahero/pkg/files"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
@@ -88,6 +90,7 @@ func PlanCmd() *cobra.Command {
 				DeploySeedData: v.GetBool("seed-data"),
 			}
 
+			specsFromFiles := []types.Spec{}
 			if fi.Mode().IsDir() {
 				err := filepath.Walk(v.GetString("spec-file"), func(path string, info os.FileInfo, err error) error {
 					isHidden, err := files.IsHidden(path)
@@ -106,9 +109,28 @@ func PlanCmd() *cobra.Command {
 						return nil
 					}
 
-					statements, err := db.PlanSyncFromFile(path, v.GetString("spec-type"))
+					specContents, err := ioutil.ReadFile(filepath.Clean(path))
 					if err != nil {
-						return err
+						return errors.Wrap(err, "failed to read file")
+					}
+					specsFromFiles = append(specsFromFiles, types.Spec{
+						SourceFilename: path,
+						Spec:           specContents,
+					})
+
+					return nil
+				})
+
+				if err != nil {
+					return errors.Wrap(err, "failed to walk directory")
+				}
+
+				db.SortSpecs(specsFromFiles)
+
+				for _, spec := range specsFromFiles {
+					statements, err := db.PlanSync(spec.Spec, v.GetString("spec-type"))
+					if err != nil {
+						return fmt.Errorf("plan sync from file %q: %w", spec.SourceFilename, err)
 					}
 
 					if f != nil {
@@ -122,11 +144,9 @@ func PlanCmd() *cobra.Command {
 							fmt.Printf("%s;\n", statement)
 						}
 					}
+				}
 
-					return nil
-				})
-
-				return err
+				return nil
 			} else {
 				statements, err := db.PlanSyncFromFile(v.GetString("spec-file"), v.GetString("spec-type"))
 				if err != nil {
diff --git a/pkg/database/database.go b/pkg/database/database.go
index b4dbb72e..3aa0ef83 100644
--- a/pkg/database/database.go
+++ b/pkg/database/database.go
@@ -5,6 +5,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"sort"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -16,6 +17,7 @@ import (
 	"github.com/schemahero/schemahero/pkg/database/rqlite"
 	"github.com/schemahero/schemahero/pkg/database/sqlite"
 	"github.com/schemahero/schemahero/pkg/database/timescaledb"
+	"github.com/schemahero/schemahero/pkg/database/types"
 	"github.com/schemahero/schemahero/pkg/logger"
 	"go.uber.org/zap"
 	"gopkg.in/yaml.v2"
@@ -173,6 +175,15 @@ func (d *Database) PlanSyncFromFile(filename string, specType string) ([]string,
 		return nil, errors.Wrap(err, "failed to read file")
 	}
 
+	plan, err := d.PlanSync(specContents, specType)
+	if err != nil {
+		return nil, errors.Wrap(err, "failed to plan sync")
+	}
+
+	return plan, nil
+}
+
+func (d *Database) PlanSync(specContents []byte, specType string) ([]string, error) {
 	// Try GVK first and fall back to plain spec for backwards compatibility
 	plan, err := d.planGVKSync(specContents)
 	if err == nil {
@@ -184,13 +195,13 @@ func (d *Database) PlanSyncFromFile(filename string, specType string) ([]string,
 	if specType == "table" {
 		plan, err := d.planTableSync(specContents)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to plan table sync from %s", filename)
+			return nil, errors.Wrapf(err, "failed to plan table sync")
 		}
 		return plan, nil
 	} else if specType == "type" {
 		plan, err := d.planTypeSync(specContents)
 		if err != nil {
-			return nil, errors.Wrapf(err, "failed to plan type sync from %s", filename)
+			return nil, errors.Wrapf(err, "failed to plan type sync")
 		}
 		return plan, nil
 	}
@@ -246,6 +257,13 @@ func (d *Database) planTableSync(specContents []byte) ([]string, error) {
 	return d.PlanSyncTableSpec(spec)
 }
 
+func (d *Database) SortSpecs(specs []types.Spec) {
+	switch d.Driver {
+	case "postgres", "timescaledb":
+		sort.Sort(types.Specs(specs))
+	}
+}
+
 func (d *Database) PlanSyncViewSpec(spec *schemasv1alpha4.ViewSpec) ([]string, error) {
 	if spec.Schema == nil {
 		return []string{}, nil
diff --git a/pkg/database/types/spec.go b/pkg/database/types/spec.go
new file mode 100644
index 00000000..eedd95d5
--- /dev/null
+++ b/pkg/database/types/spec.go
@@ -0,0 +1,50 @@
+package types
+
+import (
+	"github.com/schemahero/schemahero/pkg/client/schemaheroclientset/scheme"
+)
+
+type Spec struct {
+	SourceFilename string
+	Spec           []byte
+}
+
+type Specs []Spec
+
+// Spec sort interface implementation
+func (s Specs) Len() int {
+	return len(s)
+}
+
+func (s Specs) Swap(i, j int) {
+	s[i], s[j] = s[j], s[i]
+}
+
+// Ensure tables are processed before views, secondary sort is by file name
+func (s Specs) Less(i, j int) bool {
+	decode := scheme.Codecs.UniversalDeserializer().Decode
+
+	_, gvkI, err := decode(s[i].Spec, nil, nil)
+	if err != nil {
+		return s[i].SourceFilename < s[j].SourceFilename
+	}
+
+	_, gvkJ, err := decode(s[j].Spec, nil, nil)
+	if err != nil {
+		return s[i].SourceFilename < s[j].SourceFilename
+	}
+
+	if gvkI.Group == "schemas.schemahero.io" && gvkJ.Group == "schemas.schemahero.io" {
+		if gvkI.Kind == gvkJ.Kind {
+			return s[i].SourceFilename < s[j].SourceFilename
+		}
+		if gvkI.Kind == "Table" {
+			return true
+		}
+		if gvkJ.Kind == "Table" {
+			return false
+		}
+	}
+
+	return s[i].SourceFilename < s[j].SourceFilename
+}


Test patch:
diff --git a/pkg/database/database_test.go b/pkg/database/database_test.go
index ff653d42..8f03fa50 100644
--- a/pkg/database/database_test.go
+++ b/pkg/database/database_test.go
@@ -3,6 +3,7 @@ package database
 import (
 	"testing"
 
+	"github.com/schemahero/schemahero/pkg/database/types"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -55,3 +56,113 @@ func Test_GetStatementsFromDDL(t *testing.T) {
 		})
 	}
 }
+
+// Test the SortSpecs function
+func TestSortSpecs(t *testing.T) {
+	tests := []struct {
+		name   string
+		driver string
+		specs  []types.Spec
+		want   []types.Spec
+	}{
+		{
+			name:   "sort views and table",
+			driver: "postgres",
+			specs: []types.Spec{
+				{
+					SourceFilename: "table1.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: Table
+metadata:
+  name: table1
+spec: {}`,
+					),
+				},
+				{
+					SourceFilename: "view1.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: View
+metadata:
+  name: view1
+spec:{}`,
+					),
+				},
+				{
+					SourceFilename: "table2.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: Table
+metadata:
+  name: table2
+spec: {}`,
+					),
+				},
+				{
+					SourceFilename: "view2.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: View
+metadata:
+  name: view2
+spec:{}`,
+					),
+				},
+			},
+			want: []types.Spec{
+				{
+					SourceFilename: "table1.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: Table
+metadata:
+  name: table1
+spec: {}`,
+					),
+				},
+				{
+					SourceFilename: "table2.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: Table
+metadata:
+  name: table2
+spec: {}`,
+					),
+				},
+				{
+					SourceFilename: "view1.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: View
+metadata:
+  name: view1
+spec:{}`,
+					),
+				},
+				{
+					SourceFilename: "view2.yaml",
+					Spec: []byte(`
+apiVersion: schemas.schemahero.io/v1alpha4
+kind: View
+metadata:
+  name: view2
+spec:{}`,
+					),
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			db := &Database{
+				Driver: test.driver,
+			}
+			db.SortSpecs(test.specs)
+
+			assert.Equal(t, test.want, test.specs)
+		})
+	}
+}