Post testing tweaks and fixes
This commit is contained in:
+194
-10
@@ -1,6 +1,9 @@
|
||||
package dal
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
Alteration struct {
|
||||
@@ -36,7 +39,7 @@ type (
|
||||
|
||||
AttributeReEncode struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To *Attribute `json:"to"`
|
||||
To Codec `json:"to"`
|
||||
}
|
||||
|
||||
ModelAdd struct {
|
||||
@@ -52,24 +55,69 @@ type (
|
||||
// This is required since the Type inside the Attribute is an interface and we
|
||||
// need to help the encoding/json a bit.
|
||||
auxAttributeReType struct {
|
||||
Attr *Attribute
|
||||
To *auxAttributeType
|
||||
Attr *Attribute `json:"attr"`
|
||||
To *auxAttributeType `json:"to"`
|
||||
}
|
||||
|
||||
// auxAttributeReEncode is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since the Codec inside the Attribute is an interface and we
|
||||
// need to help the encoding/json a bit.
|
||||
auxAttributeReEncode struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To *auxStoreCodec `json:"to"`
|
||||
}
|
||||
)
|
||||
|
||||
// Merge merges the two alteration slices
|
||||
func (a AlterationSet) Merge(b AlterationSet) (c AlterationSet) {
|
||||
// @todo don't blindly append the two slices since there can be duplicates
|
||||
// or overlapping alterations which would cause needles processing
|
||||
func (aa AlterationSet) Merge(bb AlterationSet) (cc AlterationSet) {
|
||||
// @todo the Merge function currently just merges the two slices together
|
||||
// and removes any duplicates that would occur due to the merge.
|
||||
// We should also handle overlapping/transitive alterations to reduce
|
||||
// the amount of needles processing.
|
||||
//
|
||||
// A quick list of overlapping alterations:
|
||||
// * attribute A added and then renamed from A to A'
|
||||
// * attribute A renamed to A' and then renamed to A''
|
||||
// * attribute A deleted and then created
|
||||
//
|
||||
// For now we'll simply append them and worry about improvements on a later stage
|
||||
|
||||
return append(a, b...)
|
||||
cc = make(AlterationSet, 0, len(aa)+len(bb))
|
||||
skip := make(map[int]bool, (len(aa)+len(bb))/2)
|
||||
|
||||
// For each item in aa, check if it has a matching element in bb.
|
||||
// If it does, mark the bb index as skipped, if it doesn't use the aa element.
|
||||
// If a duplicate is found, the bb element is used (considered newer),
|
||||
//
|
||||
// This is sub-optimal but the slices are expected to be small and this
|
||||
// won't be ran often.
|
||||
for _, a := range aa {
|
||||
found := false
|
||||
for j, b := range bb {
|
||||
if skip[j] {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.compare(*b) {
|
||||
skip[j] = true
|
||||
cc = append(cc, b)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cc = append(cc, a)
|
||||
}
|
||||
}
|
||||
|
||||
for j, b := range bb {
|
||||
if skip[j] {
|
||||
continue
|
||||
}
|
||||
|
||||
cc = append(cc, b)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a AttributeReType) MarshalJSON() ([]byte, error) {
|
||||
@@ -191,3 +239,139 @@ func (a *AttributeReType) UnmarshalJSON(data []byte) (err error) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a AttributeReEncode) MarshalJSON() ([]byte, error) {
|
||||
aux := auxAttributeReEncode{
|
||||
Attr: a.Attr,
|
||||
To: &auxStoreCodec{},
|
||||
}
|
||||
|
||||
switch t := a.To.(type) {
|
||||
case *CodecPlain:
|
||||
aux.To.Type = "CodecPlain"
|
||||
aux.To.CodecPlain = t
|
||||
case *CodecRecordValueSetJSON:
|
||||
aux.To.Type = "CodecRecordValueSetJSON"
|
||||
aux.To.CodecRecordValueSetJSON = t
|
||||
case *CodecAlias:
|
||||
aux.To.Type = "CodecAlias"
|
||||
aux.To.CodecAlias = t
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (a *AttributeReEncode) UnmarshalJSON(data []byte) (err error) {
|
||||
aux := &auxAttributeReEncode{}
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a == nil {
|
||||
*a = AttributeReEncode{}
|
||||
}
|
||||
|
||||
a.Attr = aux.Attr
|
||||
|
||||
switch aux.To.Type {
|
||||
case "CodecPlain":
|
||||
a.To = aux.To.CodecPlain
|
||||
|
||||
case "CodecRecordValueSetJSON":
|
||||
a.To = aux.To.CodecRecordValueSetJSON
|
||||
|
||||
case "CodecAlias":
|
||||
a.To = aux.To.CodecAlias
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a Alteration) compare(b Alteration) (cmp bool) {
|
||||
if a.AttributeAdd == nil && b.AttributeAdd != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeDelete == nil && b.AttributeDelete != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeReType == nil && b.AttributeReType != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeReEncode == nil && b.AttributeReEncode != nil {
|
||||
return false
|
||||
}
|
||||
if a.ModelAdd == nil && b.ModelAdd != nil {
|
||||
return false
|
||||
}
|
||||
if a.ModelDelete == nil && b.ModelDelete != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.AttributeAdd != nil:
|
||||
return a.compareAttributeAdd(b)
|
||||
case a.AttributeDelete != nil:
|
||||
return a.compareAttributeDelete(b)
|
||||
case a.AttributeReType != nil:
|
||||
return a.compareAttributeReType(b)
|
||||
case a.AttributeReEncode != nil:
|
||||
return a.compareAttributeReEncode(b)
|
||||
case a.ModelAdd != nil:
|
||||
return a.compareModelAdd(b)
|
||||
case a.ModelDelete != nil:
|
||||
return a.compareModelDelete(b)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported alteration type %v", a))
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeAdd(b Alteration) bool {
|
||||
if a.AttributeAdd == nil || b.AttributeAdd == nil {
|
||||
return a.AttributeAdd == b.AttributeAdd
|
||||
}
|
||||
return a.AttributeAdd.Attr.Compare(b.AttributeAdd.Attr)
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeDelete(b Alteration) bool {
|
||||
if a.AttributeDelete == nil || b.AttributeDelete == nil {
|
||||
return a.AttributeDelete == b.AttributeDelete
|
||||
}
|
||||
return a.AttributeDelete.Attr.Compare(b.AttributeDelete.Attr)
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeReType(b Alteration) bool {
|
||||
if !a.AttributeReType.Attr.Compare(b.AttributeReType.Attr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.AttributeReType == nil || b.AttributeReType == nil {
|
||||
return a.AttributeReType == b.AttributeReType
|
||||
}
|
||||
return a.AttributeReType.To.Type() == b.AttributeReType.To.Type()
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeReEncode(b Alteration) bool {
|
||||
if !a.AttributeReEncode.Attr.Compare(b.AttributeReEncode.Attr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.AttributeReEncode == nil || b.AttributeReEncode == nil {
|
||||
return a.AttributeReEncode == b.AttributeReEncode
|
||||
}
|
||||
return a.AttributeReEncode.To.Type() == b.AttributeReEncode.To.Type()
|
||||
}
|
||||
|
||||
func (a Alteration) compareModelAdd(b Alteration) bool {
|
||||
if a.ModelAdd == nil || b.ModelAdd == nil {
|
||||
return a.ModelAdd == b.ModelAdd
|
||||
}
|
||||
return a.ModelAdd.Model.Compare(*b.ModelAdd.Model)
|
||||
}
|
||||
|
||||
func (a Alteration) compareModelDelete(b Alteration) bool {
|
||||
if a.ModelDelete == nil || b.ModelDelete == nil {
|
||||
return a.ModelDelete == b.ModelDelete
|
||||
}
|
||||
return a.ModelDelete.Model.Compare(*b.ModelDelete.Model)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,560 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
aa AlterationSet
|
||||
bb AlterationSet
|
||||
cc AlterationSet
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
aa: AlterationSet{},
|
||||
bb: AlterationSet{},
|
||||
cc: AlterationSet{},
|
||||
},
|
||||
{
|
||||
name: "aa empty",
|
||||
aa: AlterationSet{},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bb empty",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "un matching types",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match AttributeAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeReType",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeReType",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeText{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeText{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeReEncode",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeReEncode",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecAlias{Ident: "foo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecAlias{Ident: "foo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match ModelAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match ModelAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match ModelDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match ModelDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := require.New(t)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cc := tc.aa.Merge(tc.bb)
|
||||
req.Len(cc, len(tc.cc))
|
||||
for i, c := range cc {
|
||||
cmp := c.compare(*tc.cc[i])
|
||||
req.True(cmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReTypeMarshling(t *testing.T) {
|
||||
a := &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
},
|
||||
To: &TypeBoolean{},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &AttributeReType{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
|
||||
func TestReEncodeMarshling(t *testing.T) {
|
||||
a := &AttributeReEncode{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
},
|
||||
To: &CodecAlias{Ident: "bar"},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &AttributeReEncode{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
@@ -180,7 +180,7 @@ func (dd ModelDiffSet) Alterations() (out []*Alteration) {
|
||||
|
||||
add(&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: d.Original,
|
||||
Attr: d.Inserted,
|
||||
To: d.Inserted.Type,
|
||||
},
|
||||
})
|
||||
@@ -188,8 +188,8 @@ func (dd ModelDiffSet) Alterations() (out []*Alteration) {
|
||||
case AttributeCodecMismatch:
|
||||
add(&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: d.Original,
|
||||
To: d.Inserted,
|
||||
Attr: d.Inserted,
|
||||
To: d.Inserted.Store,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -130,6 +130,17 @@ type (
|
||||
UUID *TypeUUID `json:"uuid,omitempty"`
|
||||
}
|
||||
|
||||
// auxStoreCodec is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since some fields are interfaces
|
||||
auxStoreCodec struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
CodecPlain *CodecPlain `json:"codecPlain"`
|
||||
CodecRecordValueSetJSON *CodecRecordValueSetJSON `json:"codecRecordValueSetJSON"`
|
||||
CodecAlias *CodecAlias `json:"codecAlias"`
|
||||
}
|
||||
|
||||
AttributeSet []*Attribute
|
||||
|
||||
Index struct {
|
||||
@@ -208,6 +219,36 @@ func (a *Attribute) StoreIdent() string {
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the two attributes
|
||||
func (a *Attribute) Compare(b *Attribute) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
|
||||
out := true
|
||||
out = out && a.Ident == b.Ident
|
||||
out = out && a.Label == b.Label
|
||||
out = out && a.SensitivityLevelID == b.SensitivityLevelID
|
||||
out = out && a.MultiValue == b.MultiValue
|
||||
out = out && a.PrimaryKey == b.PrimaryKey
|
||||
out = out && a.SoftDeleteFlag == b.SoftDeleteFlag
|
||||
out = out && a.System == b.System
|
||||
out = out && a.Sortable == b.Sortable
|
||||
out = out && a.Filterable == b.Filterable
|
||||
if a.Store == nil || b.Store == nil {
|
||||
out = out && a.Store == b.Store
|
||||
} else {
|
||||
out = out && a.Store.Type() == b.Store.Type()
|
||||
}
|
||||
if a.Type == nil || b.Type == nil {
|
||||
out = out && a.Type == b.Type
|
||||
} else {
|
||||
out = out && a.Type.Type() == b.Type.Type()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (mm ModelSet) FindByResourceID(resourceID uint64) *Model {
|
||||
for _, m := range mm {
|
||||
if m.ResourceID == resourceID {
|
||||
@@ -347,6 +388,23 @@ func (m Model) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare the two models
|
||||
//
|
||||
// This only checks model metadata, the attributes are excluded.
|
||||
func (a Model) Compare(b Model) bool {
|
||||
out := true
|
||||
|
||||
out = out && a.ConnectionID == b.ConnectionID
|
||||
out = out && a.Ident == b.Ident
|
||||
out = out && a.Label == b.Label
|
||||
out = out && a.Resource == b.Resource
|
||||
out = out && a.ResourceID == b.ResourceID
|
||||
out = out && a.ResourceType == b.ResourceType
|
||||
out = out && a.SensitivityLevelID == b.SensitivityLevelID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Attribute) MarshalJSON() ([]byte, error) {
|
||||
aux := &auxAttribute{
|
||||
Ident: a.Ident,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -59,3 +61,20 @@ func TestModelFindByRefs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributeMarshling(t *testing.T) {
|
||||
a := &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &Attribute{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
|
||||
+111
-50
@@ -29,13 +29,6 @@ type (
|
||||
|
||||
connectionIssues dalIssueIndex
|
||||
modelIssues dalIssueIndex
|
||||
|
||||
Alterations alterations
|
||||
}
|
||||
|
||||
alterations interface {
|
||||
ModelAlterations(context.Context, *Model) (out []*Alteration, err error)
|
||||
SetAlterations(ctx context.Context, m *Model, stale []*Alteration, set ...*Alteration) (err error)
|
||||
}
|
||||
|
||||
FullService interface {
|
||||
@@ -50,7 +43,7 @@ type (
|
||||
RemoveConnection(ctx context.Context, ID uint64) (err error)
|
||||
|
||||
SearchModels(ctx context.Context) (out ModelSet, err error)
|
||||
ReplaceModel(ctx context.Context, model *Model) (err error)
|
||||
ReplaceModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error)
|
||||
RemoveModel(ctx context.Context, connectionID, ID uint64) (err error)
|
||||
FindModelByResourceID(connectionID uint64, resourceID uint64) *Model
|
||||
FindModelByResourceIdent(connectionID uint64, resourceType, resourceIdent string) *Model
|
||||
@@ -73,6 +66,11 @@ type (
|
||||
|
||||
var (
|
||||
gSvc *service
|
||||
|
||||
// wrapper around id.Next() that will aid service testing
|
||||
nextID = func() uint64 {
|
||||
return id.Next()
|
||||
}
|
||||
)
|
||||
|
||||
// New creates a DAL service with the primary connection
|
||||
@@ -682,7 +680,7 @@ func (svc *service) SearchModels(ctx context.Context) (out ModelSet, err error)
|
||||
// ReplaceModel adds new or updates an existing model
|
||||
//
|
||||
// We rely on the user to provide stable and valid model definitions.
|
||||
func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error) {
|
||||
func (svc *service) ReplaceModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error) {
|
||||
var (
|
||||
ID = model.ResourceID
|
||||
connection = svc.GetConnectionByID(model.ConnectionID)
|
||||
@@ -747,7 +745,7 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
|
||||
}
|
||||
|
||||
// Add to registry
|
||||
// @note models should be added to the registry regardless of issues
|
||||
// Models should be added to the registry regardless of issues
|
||||
svc.addModelToRegistry(model, upd)
|
||||
log.Debug(
|
||||
"added to registry",
|
||||
@@ -759,48 +757,13 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
|
||||
return
|
||||
}
|
||||
|
||||
// Get alterations
|
||||
// - base from the model diff
|
||||
df := oldModel.Diff(model)
|
||||
batchID := id.Next()
|
||||
aa := df.Alterations()
|
||||
for _, a := range aa {
|
||||
a.BatchID = batchID
|
||||
a.Resource = model.Resource
|
||||
a.ResourceType = model.ResourceType
|
||||
a.ConnectionID = model.ConnectionID
|
||||
}
|
||||
// - merge with existing
|
||||
baseAa, err := svc.Alterations.ModelAlterations(ctx, model)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// - cleanup to remove duplicates, squash overlapping changes, ...
|
||||
aa = svc.mergeAlterations(baseAa, aa)
|
||||
if len(aa) > 0 {
|
||||
batchID = aa[0].BatchID
|
||||
}
|
||||
// - updated with an assertion over the connection
|
||||
aa, err = connection.connection.AssertSchemaAlterations(ctx, model, aa...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, a := range aa {
|
||||
a.BatchID = batchID
|
||||
}
|
||||
|
||||
err = svc.Alterations.SetAlterations(ctx, model, baseAa, aa...)
|
||||
newAlts, batchID, err := svc.getSchemaAlterations(ctx, connection, currentAlts, oldModel, model)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(aa) > 0 {
|
||||
issues.addModelIssue(model.ResourceID, Issue{
|
||||
err: errModelRequiresAlteration(connection.ID, model.ResourceID, batchID),
|
||||
Meta: map[string]any{
|
||||
"batchID": strconv.FormatUint(batchID, 10),
|
||||
},
|
||||
})
|
||||
if len(newAlts) > 0 {
|
||||
svc.setAlterationsModelIssue(issues, batchID, connection, model, newAlts)
|
||||
log.Info("not adding to store: alterations required", zap.Error(err))
|
||||
return
|
||||
}
|
||||
@@ -853,9 +816,9 @@ func (svc *service) ApplyAlteration(ctx context.Context, alts ...*Alteration) (e
|
||||
return nil, fmt.Errorf("connection not found")
|
||||
}
|
||||
|
||||
model := svc.getModelByRef(ModelRef{Resource: resource, ResourceType: resourceType})
|
||||
model := svc.getModelByRef(ModelRef{Resource: resource, ResourceType: resourceType, ConnectionID: connectionID})
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("model not found xd")
|
||||
return nil, fmt.Errorf("model not found")
|
||||
}
|
||||
|
||||
issues = issues.addModel(model.ResourceID)
|
||||
@@ -867,6 +830,61 @@ func (svc *service) ApplyAlteration(ctx context.Context, alts ...*Alteration) (e
|
||||
return connection.connection.ApplyAlteration(ctx, model, alts...), nil
|
||||
}
|
||||
|
||||
func (svc *service) ReloadModel(ctx context.Context, currentAlts []*Alteration, model *Model) (newAlts []*Alteration, err error) {
|
||||
var (
|
||||
issues = newIssueHelper()
|
||||
|
||||
log = svc.logger.Named("models").With(
|
||||
logger.Uint64("ID", model.ResourceID),
|
||||
zap.String("ident", model.Ident),
|
||||
zap.Any("label", model.Label),
|
||||
)
|
||||
)
|
||||
defer svc.updateIssues(issues)
|
||||
|
||||
connection := svc.GetConnectionByID(model.ConnectionID)
|
||||
if connection == nil {
|
||||
err = fmt.Errorf("connection not found")
|
||||
return
|
||||
}
|
||||
|
||||
issues = issues.addModel(model.ResourceID)
|
||||
|
||||
// @todo consider adding some logging to validators
|
||||
svc.validateModel(issues, connection, model, model)
|
||||
svc.validateAttributes(issues, model, model.Attributes...)
|
||||
|
||||
// If there are any issues at this stage, there is nothing for us to do
|
||||
if issues.hasConnectionIssues() || issues.hasModelIssues() {
|
||||
log.Warn(
|
||||
"not reloading due to issues",
|
||||
zap.Any("connection issues", svc.SearchConnectionIssues(model.ConnectionID)),
|
||||
zap.Any("model issues", svc.SearchModelIssues(model.ResourceID)),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Re-evaluate schema alterations
|
||||
// oldModel nil will force it to re-check the entire thing
|
||||
newAlts, batchID, err := svc.getSchemaAlterations(ctx, connection, currentAlts, nil, model)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(newAlts) > 0 {
|
||||
svc.setAlterationsModelIssue(issues, batchID, connection, model, newAlts)
|
||||
log.Info("not adding to store: alterations required", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = connection.connection.UpdateModel(ctx, model, model)
|
||||
if err != nil {
|
||||
log.Error("failed with errors", zap.Error(err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// RemoveModel removes the given model from DAL
|
||||
//
|
||||
// @todo potentially add more interaction with the connection as in letting it know a model was removed.
|
||||
@@ -1249,3 +1267,46 @@ func (svc *service) mergeAlterations(base, added AlterationSet) (out AlterationS
|
||||
|
||||
return base.Merge(added)
|
||||
}
|
||||
|
||||
func (svc *service) getSchemaAlterations(ctx context.Context, connection *ConnectionWrap, currentAlts []*Alteration, oldModel, model *Model) (newAlts []*Alteration, batchID uint64, err error) {
|
||||
// - use the diff between the two models as a starting point to see what we should do to support the change
|
||||
df := oldModel.Diff(model)
|
||||
newAlts = df.Alterations()
|
||||
batchID = nextID()
|
||||
for _, a := range newAlts {
|
||||
a.BatchID = batchID
|
||||
a.Resource = model.Resource
|
||||
a.ResourceType = model.ResourceType
|
||||
a.ConnectionID = model.ConnectionID
|
||||
}
|
||||
|
||||
// - merge stale and new alteration
|
||||
// @todo for now we're just using the newly calculated alterations as merging with
|
||||
// existing ones is not that trivial and doesn't add much value.
|
||||
// @note this merging assumes the two sets are already ok, valid, and without any
|
||||
// duplications.
|
||||
newAlts = svc.mergeAlterations(currentAlts, newAlts)
|
||||
// - run the alterations against the database to take the schema into consideration
|
||||
newAlts, err = connection.connection.AssertSchemaAlterations(ctx, model, newAlts...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// - set all of the alterations to the same batch ID
|
||||
for _, a := range newAlts {
|
||||
a.BatchID = batchID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (svc *service) setAlterationsModelIssue(issues *issueHelper, batchID uint64, connection *ConnectionWrap, model *Model, alts []*Alteration) {
|
||||
if len(alts) > 0 {
|
||||
issues.addModelIssue(model.ResourceID, Issue{
|
||||
err: errModelRequiresAlteration(connection.ID, model.ResourceID, batchID),
|
||||
Meta: map[string]any{
|
||||
"batchID": strconv.FormatUint(batchID, 10),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ type (
|
||||
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
|
||||
|
||||
ReplaceModel(context.Context, *dal.Model) error
|
||||
ReplaceModel(ctx context.Context, currentAlts []*dal.Alteration, model *dal.Model) (newAlts []*dal.Alteration, err error)
|
||||
GetConnectionByID(uint64) *dal.ConnectionWrap
|
||||
}
|
||||
|
||||
|
||||
@@ -343,12 +343,12 @@ func (n *composeModule) Encode(ctx context.Context, pl *payload) (err error) {
|
||||
|
||||
// @todo validate ident with connection's ident validator
|
||||
|
||||
if err = pl.dal.ReplaceModel(ctx, rModel); err != nil {
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, rModel); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = pl.dal.ReplaceModel(ctx, model); err != nil {
|
||||
if _, err = pl.dal.ReplaceModel(ctx, nil, model); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user