3
0

Update pkg/dal to handle schema alterations

This commit is contained in:
Tomaž Jerman 2023-06-04 07:28:56 +02:00
parent 0695eb7118
commit 4224ba9828
8 changed files with 730 additions and 236 deletions

View File

@ -0,0 +1,193 @@
package dal
import "encoding/json"
type (
Alteration struct {
ID uint64
BatchID uint64
DependsOn uint64
Resource string
ResourceType string
ConnectionID uint64
AttributeAdd *AttributeAdd
AttributeDelete *AttributeDelete
AttributeReType *AttributeReType
AttributeReEncode *AttributeReEncode
ModelAdd *ModelAdd
ModelDelete *ModelDelete
}
AlterationSet []*Alteration
AttributeAdd struct {
Attr *Attribute `json:"attr"`
}
AttributeDelete struct {
Attr *Attribute `json:"attr"`
}
AttributeReType struct {
Attr *Attribute `json:"attr"`
To Type `json:"to"`
}
AttributeReEncode struct {
Attr *Attribute `json:"attr"`
To *Attribute `json:"to"`
}
ModelAdd struct {
Model *Model `json:"model"`
}
ModelDelete struct {
Model *Model `json:"model"`
}
// auxAttributeType is a helper struct used for marshaling/unmarshaling
//
// 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
}
)
// 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
//
// 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...)
}
func (a AttributeReType) MarshalJSON() ([]byte, error) {
aux := auxAttributeReType{
Attr: a.Attr,
To: &auxAttributeType{},
}
switch t := a.To.(type) {
case *TypeID:
aux.To.Type = "ID"
aux.To.ID = t
case *TypeRef:
aux.To.Type = "Ref"
aux.To.Ref = t
case *TypeTimestamp:
aux.To.Type = "Timestamp"
aux.To.Timestamp = t
case *TypeTime:
aux.To.Type = "Time"
aux.To.Time = t
case *TypeDate:
aux.To.Type = "Date"
aux.To.Date = t
case *TypeNumber:
aux.To.Type = "Number"
aux.To.Number = t
case *TypeText:
aux.To.Type = "Text"
aux.To.Text = t
case *TypeBoolean:
aux.To.Type = "Boolean"
aux.To.Boolean = t
case *TypeEnum:
aux.To.Type = "Enum"
aux.To.Enum = t
case *TypeGeometry:
aux.To.Type = "Geometry"
aux.To.Geometry = t
case *TypeJSON:
aux.To.Type = "JSON"
aux.To.JSON = t
case *TypeBlob:
aux.To.Type = "Blob"
aux.To.Blob = t
case *TypeUUID:
aux.To.Type = "UUID"
aux.To.UUID = t
}
return json.Marshal(aux)
}
func (a *AttributeReType) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttributeReType{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = AttributeReType{}
}
a.Attr = aux.Attr
switch aux.To.Type {
case "ID":
a.To = aux.To.ID
case "Ref":
a.To = aux.To.Ref
case "Timestamp":
a.To = aux.To.Timestamp
case "Time":
a.To = aux.To.Time
case "Date":
a.To = aux.To.Date
case "Number":
a.To = aux.To.Number
case "Text":
a.To = aux.To.Text
case "Boolean":
a.To = aux.To.Boolean
case "Enum":
a.To = aux.To.Enum
case "Geometry":
a.To = aux.To.Geometry
case "JSON":
a.To = aux.To.JSON
case "Blob":
a.To = aux.To.Blob
case "UUID":
a.To = aux.To.UUID
}
return
}

View File

@ -52,11 +52,17 @@ type (
}
)
func (*CodecPlain) Type() AttributeCodecType { return "corteza::dal:attribute-codec:plain" }
const (
AttributeCodecPlain AttributeCodecType = "corteza::dal:attribute-codec:plain"
AttributeCodecRecordValueSetJSON AttributeCodecType = "corteza::dal:attribute-codec:record-value-set-json"
AttributeCodecAlias AttributeCodecType = "corteza::dal:attribute-codec:alias"
)
func (*CodecPlain) Type() AttributeCodecType { return AttributeCodecPlain }
func (*CodecRecordValueSetJSON) Type() AttributeCodecType {
return "corteza::dal:attribute-codec:record-value-set-json"
return AttributeCodecRecordValueSetJSON
}
func (*CodecAlias) Type() AttributeCodecType { return "corteza::dal:attribute-codec:alias" }
func (*CodecAlias) Type() AttributeCodecType { return AttributeCodecAlias }
func (*CodecPlain) SingleValueOnly() bool { return true }
func (*CodecRecordValueSetJSON) SingleValueOnly() bool { return false }

View File

@ -28,6 +28,13 @@ const (
// Diff calculates the diff between models a and b where a is used as base
func (a *Model) Diff(b *Model) (out ModelDiffSet) {
if a == nil {
a = &Model{}
}
if b == nil {
b = &Model{}
}
bIndex := make(map[string]struct {
found bool
attr *Attribute
@ -124,3 +131,69 @@ func (a *Model) Diff(b *Model) (out ModelDiffSet) {
return
}
func (dd ModelDiffSet) Alterations() (out []*Alteration) {
add := func(a *Alteration) {
out = append(out, a)
}
for _, d := range dd {
switch d.Type {
case AttributeMissing:
if d.Inserted == nil {
// @todo if this was the last attribute we can consider dropping this column
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
break
}
add(&Alteration{
AttributeDelete: &AttributeDelete{
Attr: d.Original,
},
})
} else {
if d.Inserted.Store.Type() == AttributeCodecRecordValueSetJSON {
add(&Alteration{
AttributeAdd: &AttributeAdd{
Attr: &Attribute{
Ident: d.Inserted.StoreIdent(),
Type: &TypeJSON{Nullable: false},
Store: &CodecPlain{},
},
},
})
} else {
add(&Alteration{
AttributeAdd: &AttributeAdd{
Attr: d.Inserted,
},
})
}
}
case AttributeTypeMissmatch:
// @todo we might have to do some validation earlier on
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
break
}
add(&Alteration{
AttributeReType: &AttributeReType{
Attr: d.Original,
To: d.Inserted.Type,
},
})
case AttributeCodecMismatch:
add(&Alteration{
AttributeReEncode: &AttributeReEncode{
Attr: d.Original,
To: d.Inserted,
},
})
}
}
return
}

View File

@ -7,7 +7,6 @@ import (
"regexp"
"strings"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/ql"
"go.uber.org/zap"
@ -80,12 +79,12 @@ type (
// Only metadata (such as idents) are affected; attributes can not be changed here
UpdateModel(ctx context.Context, old *Model, new *Model) error
// UpdateModelAttribute requests for the model attribute change
//
// Specific operations require data transformations (type change).
// Some basic ops. should be implemented on DB driver level, but greater controll can be
// achieved via the trans functions.
UpdateModelAttribute(ctx context.Context, sch *Model, diff *ModelDiff, hasRecords bool, trans ...TransformationFunction) error
// AssertSchemaAlterations returns a new set of Alterations based on what the underlying
// schema already provides -- it discards alterations for column additions that already exist, etc.
AssertSchemaAlterations(ctx context.Context, sch *Model, aa ...*Alteration) ([]*Alteration, error)
// ApplyAlteration applies the given alterations to the underlying schema
ApplyAlteration(ctx context.Context, sch *Model, aa ...*Alteration) []error
}
ConnectionCloser interface {
@ -94,8 +93,6 @@ type (
Close(ctx context.Context) error
}
TransformationFunction func(*Model, Attribute, expr.TypedValue) (expr.TypedValue, bool, error)
// Store provides an interface which CRS uses to interact with the underlying database
ValueGetter interface {

View File

@ -2,6 +2,7 @@ package dal
import (
"fmt"
"github.com/cortezaproject/corteza/server/pkg/errors"
)
@ -67,6 +68,9 @@ func errModelCreateGreaterAttributeSensitivityLevel(connectionID, modelID, attrS
func errModelCreateConnectionModelUnsupported(connectionID, modelID uint64) error {
return fmt.Errorf("cannot create model %d on connection %d: model already exists for connection but is not compatible with provided definition", modelID, connectionID)
}
func errModelCreateInvalidIdent(connectionID, modelID uint64, ident string) error {
return fmt.Errorf("cannot create model %d on connection %d: malformed model ident %s", modelID, connectionID, ident)
}
// - update
func errModelUpdateProblematicConnection(connectionID, modelID uint64) error {
@ -94,6 +98,11 @@ func errModelUpdateGreaterSensitivityLevel(connectionID, modelID, modelSensitivi
return fmt.Errorf("cannot update model %d on connection %d: sensitivity level %d exceeds connection supported sensitivity level %d", modelID, connectionID, modelSensitivityLevelID, connSensitivityLevelID)
}
// - alterations
func errModelRequiresAlteration(connectionID, modelID, batchID uint64) error {
return fmt.Errorf("model %d on connection %d requires schema alterations: alteration batchID %d", modelID, connectionID, batchID)
}
// Attribute errors
// - Update
func errAttributeUpdateProblematicConnection(connectionID, modelID uint64) error {

View File

@ -3,11 +3,14 @@ package dal
import "go.uber.org/zap"
type (
issue struct {
kind issueKind
err error
Issue struct {
err error
Issue string `json:"issue,omitempty"`
Kind issueKind `json:"kind,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
}
issueSet []issue
issueSet []Issue
issueHelper struct {
// these two will be used to help clear out unneeded errors
@ -34,27 +37,12 @@ func newIssueHelper() *issueHelper {
}
}
func makeIssue(kind issueKind, err error) issue {
return issue{
kind: kind,
err: err,
}
func (svc *service) SearchConnectionIssues(connectionID uint64) (out []Issue) {
return svc.connectionIssues[connectionID]
}
func (svc *service) SearchConnectionIssues(connectionID uint64) (out []error) {
for _, issue := range svc.connectionIssues[connectionID] {
out = append(out, issue.err)
}
return
}
func (svc *service) SearchModelIssues(resourceID uint64) (out []error) {
for _, issue := range svc.modelIssues[resourceID] {
out = append(out, issue.err)
}
return
func (svc *service) SearchModelIssues(resourceID uint64) (out []Issue) {
return svc.modelIssues[resourceID]
}
func (svc *service) hasConnectionIssues(connectionID uint64) bool {
@ -95,12 +83,24 @@ func (rd *issueHelper) addModel(modelID uint64) *issueHelper {
return rd
}
func (rd *issueHelper) addConnectionIssue(connectionID uint64, err error) {
rd.connectionIssues[connectionID] = append(rd.connectionIssues[connectionID], makeIssue(connectionIssue, err))
func (rd *issueHelper) addConnectionIssue(connectionID uint64, i Issue) {
i.Kind = connectionIssue
i.Issue = i.err.Error()
rd.connectionIssues[connectionID] = append(rd.connectionIssues[connectionID], i)
}
func (rd *issueHelper) addModelIssue(resourceID uint64, err error) {
rd.modelIssues[resourceID] = append(rd.modelIssues[resourceID], makeIssue(modelIssue, err))
func (rd *issueHelper) addModelIssue(resourceID uint64, i Issue) {
i.Kind = modelIssue
i.Issue = i.err.Error()
rd.modelIssues[resourceID] = append(rd.modelIssues[resourceID], i)
}
func (rd *issueHelper) hasConnectionIssues() bool {
return len(rd.connectionIssues) > 0
}
func (rd *issueHelper) hasModelIssues() bool {
return len(rd.modelIssues) > 0
}
func (a *issueHelper) mergeWith(b *issueHelper) {

View File

@ -1,6 +1,7 @@
package dal
import (
"encoding/json"
"fmt"
"strings"
@ -79,6 +80,56 @@ type (
Type Type
}
// auxAttribute is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttribute struct {
Ident string `json:"ident"`
Label string `json:"label"`
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
MultiValue bool `json:"multiValue"`
PrimaryKey bool `json:"primaryKey"`
SoftDeleteFlag bool `json:"softDeleteFlag"`
System bool `json:"system"`
Sortable bool `json:"sortable"`
Filterable bool `json:"filterable"`
Store *auxAttributeStore `json:"store"`
Type *auxAttributeType `json:"type"`
}
// auxAttributeStore is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttributeStore struct {
Type string `json:"type"`
Plain *CodecPlain `json:"plain,omitempty"`
RecordValueSetJSON *CodecRecordValueSetJSON `json:"recordValueSetJSON,omitempty"`
Alias *CodecAlias `json:"alias,omitempty"`
}
// auxAttributeType is a helper struct used for marshaling/unmarshaling
//
// This is required since some fields are interfaces
auxAttributeType struct {
Type string `json:"type"`
ID *TypeID `json:"id,omitempty"`
Ref *TypeRef `json:"ref,omitempty"`
Timestamp *TypeTimestamp `json:"timestamp,omitempty"`
Time *TypeTime `json:"time,omitempty"`
Date *TypeDate `json:"date,omitempty"`
Number *TypeNumber `json:"number,omitempty"`
Text *TypeText `json:"text,omitempty"`
Boolean *TypeBoolean `json:"boolean,omitempty"`
Enum *TypeEnum `json:"enum,omitempty"`
Geometry *TypeGeometry `json:"geometry,omitempty"`
JSON *TypeJSON `json:"jSON,omitempty"`
Blob *TypeBlob `json:"blob,omitempty"`
UUID *TypeUUID `json:"uuid,omitempty"`
}
AttributeSet []*Attribute
Index struct {
@ -295,3 +346,175 @@ func (m Model) Validate() error {
return nil
}
func (a *Attribute) MarshalJSON() ([]byte, error) {
aux := &auxAttribute{
Ident: a.Ident,
Label: a.Label,
SensitivityLevelID: a.SensitivityLevelID,
MultiValue: a.MultiValue,
PrimaryKey: a.PrimaryKey,
SoftDeleteFlag: a.SoftDeleteFlag,
System: a.System,
Sortable: a.Sortable,
Filterable: a.Filterable,
Store: &auxAttributeStore{},
Type: &auxAttributeType{},
}
switch s := a.Store.(type) {
case *CodecPlain:
aux.Store.Type = "plain"
aux.Store.Plain = s
case *CodecRecordValueSetJSON:
aux.Store.Type = "recordValueSetJSON"
aux.Store.RecordValueSetJSON = s
case *CodecAlias:
aux.Store.Type = "alias"
aux.Store.Alias = s
default:
return nil, fmt.Errorf("unknown store codec type: %T", s)
}
switch t := a.Type.(type) {
case *TypeID:
aux.Type.Type = "ID"
aux.Type.ID = t
case *TypeRef:
aux.Type.Type = "Ref"
aux.Type.Ref = t
case *TypeTimestamp:
aux.Type.Type = "Timestamp"
aux.Type.Timestamp = t
case *TypeTime:
aux.Type.Type = "Time"
aux.Type.Time = t
case *TypeDate:
aux.Type.Type = "Date"
aux.Type.Date = t
case *TypeNumber:
aux.Type.Type = "Number"
aux.Type.Number = t
case *TypeText:
aux.Type.Type = "Text"
aux.Type.Text = t
case *TypeBoolean:
aux.Type.Type = "Boolean"
aux.Type.Boolean = t
case *TypeEnum:
aux.Type.Type = "Enum"
aux.Type.Enum = t
case *TypeGeometry:
aux.Type.Type = "Geometry"
aux.Type.Geometry = t
case *TypeJSON:
aux.Type.Type = "JSON"
aux.Type.JSON = t
case *TypeBlob:
aux.Type.Type = "Blob"
aux.Type.Blob = t
case *TypeUUID:
aux.Type.Type = "UUID"
aux.Type.UUID = t
default:
return nil, fmt.Errorf("unknown attribute type type: %T", t)
}
return json.Marshal(aux)
}
func (a *Attribute) UnmarshalJSON(data []byte) (err error) {
aux := &auxAttribute{}
err = json.Unmarshal(data, &aux)
if err != nil {
return err
}
if a == nil {
*a = Attribute{}
}
a.Ident = aux.Ident
a.Label = aux.Label
a.SensitivityLevelID = aux.SensitivityLevelID
a.MultiValue = aux.MultiValue
a.PrimaryKey = aux.PrimaryKey
a.SoftDeleteFlag = aux.SoftDeleteFlag
a.System = aux.System
a.Sortable = aux.Sortable
a.Filterable = aux.Filterable
switch aux.Store.Type {
case "plain":
a.Store = aux.Store.Plain
case "recordValueSetJSON":
a.Store = aux.Store.RecordValueSetJSON
case "alias":
a.Store = aux.Store.Alias
}
switch aux.Type.Type {
case "ID":
a.Type = aux.Type.ID
case "Ref":
a.Type = aux.Type.Ref
case "Timestamp":
a.Type = aux.Type.Timestamp
case "Time":
a.Type = aux.Type.Time
case "Date":
a.Type = aux.Type.Date
case "Number":
a.Type = aux.Type.Number
case "Text":
a.Type = aux.Type.Text
case "Boolean":
a.Type = aux.Type.Boolean
case "Enum":
a.Type = aux.Type.Enum
case "Geometry":
a.Type = aux.Type.Geometry
case "JSON":
a.Type = aux.Type.JSON
case "Blob":
a.Type = aux.Type.Blob
case "UUID":
a.Type = aux.Type.UUID
default:
return fmt.Errorf("unknown attribute type type: %s", aux.Type.Type)
}
return
}

View File

@ -3,10 +3,10 @@ package dal
import (
"context"
"fmt"
"regexp"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/logger"
"go.uber.org/zap"
)
@ -29,6 +29,13 @@ 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 {
@ -45,7 +52,6 @@ type (
SearchModels(ctx context.Context) (out ModelSet, err error)
ReplaceModel(ctx context.Context, model *Model) (err error)
RemoveModel(ctx context.Context, connectionID, ID uint64) (err error)
ReplaceModelAttribute(ctx context.Context, model *Model, dif *ModelDiff, hasRecords bool, trans ...TransformationFunction) (err error)
FindModelByResourceID(connectionID uint64, resourceID uint64) *Model
FindModelByResourceIdent(connectionID uint64, resourceType, resourceIdent string) *Model
FindModelByIdent(connectionID uint64, ident string) *Model
@ -60,8 +66,8 @@ type (
Run(ctx context.Context, pp Pipeline) (iter Iterator, err error)
Dryrun(ctx context.Context, pp Pipeline) (err error)
SearchConnectionIssues(connectionID uint64) (out []error)
SearchModelIssues(resourceID uint64) (out []error)
SearchConnectionIssues(connectionID uint64) (out []Issue)
SearchModelIssues(resourceID uint64) (out []Issue)
}
)
@ -319,7 +325,9 @@ func (svc *service) ReplaceConnection(ctx context.Context, conn *ConnectionWrap,
// Sensitivity level validations
if !svc.sensitivityLevels.includes(conn.Config.SensitivityLevelID) {
issues.addConnectionIssue(ID, errConnectionCreateMissingSensitivityLevel(ID, conn.Config.SensitivityLevelID))
issues.addConnectionIssue(ID, Issue{
err: errConnectionCreateMissingSensitivityLevel(ID, conn.Config.SensitivityLevelID),
})
}
if oldConn = svc.GetConnectionByID(ID); oldConn != nil {
@ -335,7 +343,9 @@ func (svc *service) ReplaceConnection(ctx context.Context, conn *ConnectionWrap,
// - sensitivity levels
if !svc.sensitivityLevels.isSubset(model.SensitivityLevelID, conn.Config.SensitivityLevelID) {
issues.addConnectionIssue(ID, fmt.Errorf("cannot update connection %d: new connection sensitivity level does not support model %d", ID, model.ResourceID))
issues.addConnectionIssue(ID, Issue{
err: fmt.Errorf("cannot update connection %d: new connection sensitivity level does not support model %d", ID, model.ResourceID),
})
errored = true
}
}
@ -349,7 +359,9 @@ func (svc *service) ReplaceConnection(ctx context.Context, conn *ConnectionWrap,
// close old connection
if cc, ok := oldConn.connection.(ConnectionCloser); ok {
if err = cc.Close(ctx); err != nil {
issues.addConnectionIssue(ID, err)
issues.addConnectionIssue(ID, Issue{
err: err,
})
return nil
}
@ -363,7 +375,9 @@ func (svc *service) ReplaceConnection(ctx context.Context, conn *ConnectionWrap,
conn.connection, err = connect(ctx, svc.logger, svc.inDev, conn.params)
if err != nil {
log.Warn("could not connect", zap.Error(err))
issues.addConnectionIssue(ID, err)
issues.addConnectionIssue(ID, Issue{
err: err,
})
} else {
log.Debug("connected")
}
@ -667,18 +681,17 @@ func (svc *service) SearchModels(ctx context.Context) (out ModelSet, err error)
// ReplaceModel adds new or updates an existing model
//
// ReplaceModel only affects the metadata (the connection, identifier, ...)
// and leaves attributes untouched.
// Use the ReplaceModelAttribute to upsert attributes.
//
// We rely on the user to provide stable and valid model definitions.
func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error) {
var (
ID = model.ResourceID
oldModel *Model
issues = newIssueHelper().addModel(ID)
auxIssues = newIssueHelper()
upd bool
ID = model.ResourceID
connection = svc.GetConnectionByID(model.ConnectionID)
oldModel *Model
issues = newIssueHelper().addModel(ID)
upd bool
modelIssues bool
connectionIssues bool
log = svc.logger.Named("models").With(
logger.Uint64("ID", ID),
@ -693,8 +706,8 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
model.ConnectionID = svc.defConnID
}
// Check if update
if oldModel = svc.FindModelByResourceID(model.ConnectionID, model.ResourceID); oldModel != nil {
// Check if we're creating or updating the model
if oldModel = svc.FindModel(model.ToFilter()); oldModel != nil {
log.Debug("found existing")
if oldModel.ConnectionID != model.ConnectionID {
@ -704,54 +717,134 @@ func (svc *service) ReplaceModel(ctx context.Context, model *Model) (err error)
upd = true
}
// Validation
svc.validateModel(issues, model, oldModel)
for _, attr := range model.Attributes {
svc.validateAttribute(issues, model, attr)
}
// @todo consider adding some logging to validators
svc.validateModel(issues, connection, model, oldModel)
svc.validateAttributes(issues, model, model.Attributes...)
// Add to connection
connectionIssues := svc.hasConnectionIssues(model.ConnectionID)
connectionIssues = issues.hasConnectionIssues()
if connectionIssues {
log.Warn(
"not adding to connection due to connection issues",
"not adding to store due to connection issues",
zap.Any("issues", svc.SearchConnectionIssues(model.ConnectionID)),
)
}
modelIssues := svc.hasModelIssues(model.ResourceID)
modelIssues = issues.hasModelIssues()
if modelIssues {
log.Warn(
"not adding to connection due to model issues",
"not adding to store due to model issues",
zap.Any("issues", svc.SearchModelIssues(model.ResourceID)),
)
}
connection := svc.GetConnectionByID(model.ConnectionID)
if !modelIssues && !connectionIssues {
if !checkIdent(model.Ident, connection.Config.ModelIdentCheck...) {
log.Warn("can not add model to connection, invalid ident")
return nil
}
auxIssues, err = svc.registerModelToConnection(ctx, connection, model)
issues.mergeWith(auxIssues)
if err != nil {
log.Error("failed with errors", zap.Error(err))
return
}
}
// Add to registry
// @note models should be added to the registry regardless of issues
svc.addModelToRegistry(model, upd)
log.Debug(
"added",
"added to registry",
logger.Uint64("connectionID", model.ConnectionID),
)
// Can't continue with connection alterations if there are issues already
if connectionIssues || modelIssues {
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...)
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),
},
})
log.Info("not adding to store: alterations required", zap.Error(err))
return
}
if upd {
err = connection.connection.UpdateModel(ctx, oldModel, model)
} else {
err = connection.connection.CreateModel(ctx, model)
}
if err != nil {
log.Error("failed with errors", zap.Error(err))
}
return
}
// ApplyAlteration updates the underlying schema with the requested changes
func (svc *service) ApplyAlteration(ctx context.Context, alts ...*Alteration) (errs []error, err error) {
if len(alts) == 0 {
return
}
connectionID := alts[0].ConnectionID
resource := alts[0].Resource
resourceType := alts[0].ResourceType
for _, alt := range alts {
if alt.ConnectionID != connectionID {
return nil, fmt.Errorf("alterations must be for the same connection")
}
if alt.ResourceType != resourceType {
return nil, fmt.Errorf("alterations must be for the same resource type")
}
if alt.Resource != resource {
return nil, fmt.Errorf("alterations must be for the same resource")
}
}
connection := svc.GetConnectionByID(connectionID)
if connection == nil {
return nil, fmt.Errorf("connection not found")
}
model := svc.getModelByRef(ModelRef{Resource: resource, ResourceType: resourceType})
if model == nil {
return nil, fmt.Errorf("model not found xd")
}
return connection.connection.ApplyAlteration(ctx, model, alts...), nil
}
// RemoveModel removes the given model from DAL
//
// @todo potentially add more interaction with the connection as in letting it know a model was removed.
@ -798,44 +891,64 @@ func (svc *service) RemoveModel(ctx context.Context, connectionID, ID uint64) (e
return nil
}
func (svc *service) validateModel(issues *issueHelper, model, oldModel *Model) {
func (svc *service) validateModel(issues *issueHelper, c *ConnectionWrap, model, oldModel *Model) {
// Connection ok?
if svc.hasConnectionIssues(model.ConnectionID) {
issues.addModelIssue(model.ResourceID, errModelCreateProblematicConnection(model.ConnectionID, model.ResourceID))
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateProblematicConnection(model.ConnectionID, model.ResourceID),
})
}
// Connection exists?
conn := svc.GetConnectionByID(model.ConnectionID)
if conn == nil {
issues.addModelIssue(model.ResourceID, errModelCreateMissingConnection(model.ConnectionID, model.ResourceID))
if c == nil {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateMissingConnection(model.ConnectionID, model.ResourceID),
})
}
// If ident changed, check for duplicate
if oldModel != nil && oldModel.Ident != model.Ident {
if tmp := svc.FindModelByIdent(model.ConnectionID, model.Ident); tmp == nil {
issues.addModelIssue(oldModel.ResourceID, errModelUpdateDuplicate(model.ConnectionID, model.ResourceID))
issues.addModelIssue(oldModel.ResourceID, Issue{
err: errModelUpdateDuplicate(model.ConnectionID, model.ResourceID),
})
}
}
// Sensitivity level ok and valid?
if !svc.sensitivityLevels.includes(model.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errModelCreateMissingSensitivityLevel(model.ConnectionID, model.ResourceID, model.SensitivityLevelID))
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateMissingSensitivityLevel(model.ConnectionID, model.ResourceID, model.SensitivityLevelID),
})
} else {
// Only check if it is present
if !svc.sensitivityLevels.isSubset(model.SensitivityLevelID, conn.Config.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errModelCreateGreaterSensitivityLevel(model.ConnectionID, model.ResourceID, model.SensitivityLevelID, conn.Config.SensitivityLevelID))
if !svc.sensitivityLevels.isSubset(model.SensitivityLevelID, c.Config.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateGreaterSensitivityLevel(model.ConnectionID, model.ResourceID, model.SensitivityLevelID, c.Config.SensitivityLevelID),
})
}
}
if c != nil && !checkIdent(model.Ident, c.Config.ModelIdentCheck...) {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateInvalidIdent(model.ConnectionID, model.ResourceID, model.Ident),
})
}
}
func (svc *service) validateAttribute(issues *issueHelper, model *Model, attr *Attribute) {
if !svc.sensitivityLevels.includes(attr.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errModelCreateMissingAttributeSensitivityLevel(model.ConnectionID, model.ResourceID, attr.SensitivityLevelID))
} else {
if !svc.sensitivityLevels.isSubset(attr.SensitivityLevelID, model.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errModelCreateGreaterAttributeSensitivityLevel(model.ConnectionID, model.ResourceID, attr.SensitivityLevelID, model.SensitivityLevelID))
func (svc *service) validateAttributes(issues *issueHelper, model *Model, attr ...*Attribute) {
for _, a := range attr {
if !svc.sensitivityLevels.includes(a.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateMissingAttributeSensitivityLevel(model.ConnectionID, model.ResourceID, a.SensitivityLevelID),
})
} else {
if !svc.sensitivityLevels.isSubset(a.SensitivityLevelID, model.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, Issue{
err: errModelCreateGreaterAttributeSensitivityLevel(model.ConnectionID, model.ResourceID, a.SensitivityLevelID, model.SensitivityLevelID),
})
}
}
}
}
func (svc *service) addModelToRegistry(model *Model, upd bool) {
@ -869,100 +982,6 @@ func (svc *service) removeModelFromRegistry(model *Model) {
}
}
// ReplaceModelAttribute adds new or updates an existing attribute for the given model
//
// We rely on the user to provide stable and valid attribute definitions.
func (svc *service) ReplaceModelAttribute(ctx context.Context, model *Model, diff *ModelDiff, hasRecords bool, trans ...TransformationFunction) (err error) {
svc.logger.Debug("updating model attribute", logger.Uint64("model", model.ResourceID))
var (
conn *ConnectionWrap
issues = newIssueHelper().addModel(model.ResourceID)
)
defer svc.updateIssues(issues)
if model.ConnectionID == 0 {
model.ConnectionID = svc.defConnID
}
// Validation
{
// Connection issues
if svc.hasConnectionIssues(model.ConnectionID) {
issues.addModelIssue(model.ResourceID, errAttributeUpdateProblematicConnection(model.ConnectionID, model.ResourceID))
}
// Check if it exists
auxModel := svc.FindModelByResourceID(model.ConnectionID, model.ResourceID)
if auxModel == nil {
issues.addModelIssue(model.ResourceID, errAttributeUpdateMissingModel(model.ConnectionID, model.ResourceID))
}
// In case we're deleting it we can ignore this check
if diff.Inserted != nil {
if !svc.sensitivityLevels.includes(diff.Inserted.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errAttributeUpdateMissingSensitivityLevel(model.ConnectionID, model.ResourceID, diff.Inserted.SensitivityLevelID))
} else {
if !svc.sensitivityLevels.isSubset(diff.Inserted.SensitivityLevelID, model.SensitivityLevelID) {
issues.addModelIssue(model.ResourceID, errAttributeUpdateGreaterSensitivityLevel(model.ConnectionID, model.ResourceID, diff.Inserted.SensitivityLevelID, model.SensitivityLevelID))
}
}
}
conn = svc.GetConnectionByID(model.ConnectionID)
}
// Update attribute
// Update connection
connectionIssues := svc.hasConnectionIssues(model.ConnectionID)
modelIssues := svc.hasModelIssues(model.ResourceID)
if !modelIssues && !connectionIssues {
svc.logger.Debug("updating model attribute", logger.Uint64("connection", model.ConnectionID), logger.Uint64("model", model.ResourceID))
err = conn.connection.UpdateModelAttribute(ctx, model, diff, hasRecords, trans...)
if err != nil {
issues.addModelIssue(model.ResourceID, err)
}
} else {
if connectionIssues {
svc.logger.Warn("not updating model attribute due to connection issues", logger.Uint64("connection", model.ConnectionID))
}
if modelIssues {
svc.logger.Warn("not updating model attribute due to model issues", logger.Uint64("model", model.ResourceID))
}
}
// Update registry
if diff.Original == nil {
// adding
model.Attributes = append(model.Attributes, diff.Original)
} else if diff.Original == nil {
// removing
model = svc.FindModelByResourceID(model.ConnectionID, model.ResourceID)
nSet := make(AttributeSet, 0, len(model.Attributes))
for _, attribute := range model.Attributes {
if attribute.Ident != diff.Original.Ident {
nSet = append(nSet, attribute)
}
}
model.Attributes = nSet
} else {
// updating
model = svc.FindModelByResourceID(model.ConnectionID, model.ResourceID)
for i, attribute := range model.Attributes {
if attribute.Ident == diff.Original.Ident {
model.Attributes[i] = diff.Inserted
break
}
}
}
svc.logger.Debug("updated model attribute")
return
}
// FindModelByRefs returns the model with all of the given refs matching
//
// @note refs are primarily used for DAL pipelines where steps can reference models
@ -1059,48 +1078,6 @@ func (svc *service) getConnection(connectionID uint64, oo ...Operation) (cw *Con
return
}
func (svc *service) registerModelToConnection(ctx context.Context, cw *ConnectionWrap, model *Model) (issues *issueHelper, err error) {
issues = newIssueHelper()
available, err := cw.connection.Models(ctx)
if err != nil {
issues.addConnectionIssue(model.ConnectionID, err)
return issues, nil
}
// Check if already in there
if existing := available.FindByResourceIdent(model.ResourceType, model.Resource); existing != nil {
// Assert validity
diff := existing.Diff(model)
if len(diff) > 0 {
issues.addModelIssue(model.ResourceID, errModelCreateConnectionModelUnsupported(model.ConnectionID, model.ResourceID))
return issues, nil
}
return
}
// make sure connection supports model's ident
var (
rre []*regexp.Regexp
)
for _, re := range rre {
if re.MatchString(model.Ident) {
return
}
}
// Try to add to store
err = cw.connection.CreateModel(ctx, model)
if err != nil {
issues.addModelIssue(model.ResourceID, err)
return issues, nil
}
return nil, nil
}
func (svc *service) getModelByRef(mr ModelRef) *Model {
if mr.ConnectionID == 0 {
mr.ConnectionID = svc.defConnID
@ -1234,3 +1211,19 @@ func (svc *service) analyzePipeline(ctx context.Context, pp Pipeline) (err error
return
}
func (svc *service) mergeAlterations(base, added AlterationSet) (out AlterationSet) {
var (
batchID uint64
)
if len(base) > 0 {
batchID = base[0].BatchID
}
for _, a := range base {
a.BatchID = batchID
}
return base.Merge(added)
}