Post testing tweaks and fixes

This commit is contained in:
Tomaž Jerman
2023-10-26 17:09:58 +02:00
parent 7b5e8d1aa9
commit 6795e5e0f2
15 changed files with 1225 additions and 157 deletions
+194 -10
View File
@@ -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)
}
+560
View File
@@ -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))
}
+3 -3
View File
@@ -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,
},
})
}
+58
View File
@@ -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,
+19
View File
@@ -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
View File
@@ -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),
},
})
}
}
+1 -1
View File
@@ -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
}