Skip to content

Commit

Permalink
🐛 Fix reused primary keys; generate PK.
Browse files Browse the repository at this point in the history
Signed-off-by: Jeff Ortel <[email protected]>
  • Loading branch information
jortel committed Jul 9, 2024
1 parent 8bfd399 commit 3a8f00a
Show file tree
Hide file tree
Showing 12 changed files with 1,064 additions and 2 deletions.
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
crd "github.com/konveyor/tackle2-hub/k8s/api"
"github.com/konveyor/tackle2-hub/metrics"
"github.com/konveyor/tackle2-hub/migration"
"github.com/konveyor/tackle2-hub/model"
"github.com/konveyor/tackle2-hub/reaper"
"github.com/konveyor/tackle2-hub/seed"
"github.com/konveyor/tackle2-hub/settings"
Expand Down Expand Up @@ -53,6 +54,10 @@ func Setup() (db *gorm.DB, err error) {
if err != nil {
return
}
err = database.PK.Load(db, model.ALL)
if err != nil {
return
}
return
}

Expand Down
149 changes: 149 additions & 0 deletions database/pk.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package database

import (
"errors"
"reflect"
"strings"
"sync"

"github.com/konveyor/tackle2-hub/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)

// PK singleton pk sequence.
var PK PkSequence

// PkSequence provides a primary key sequence.
type PkSequence struct {
mutex sync.Mutex
}

// Load highest key for all models.
func (r *PkSequence) Load(db *gorm.DB, models []any) (err error) {
r.mutex.Lock()
defer r.mutex.Unlock()
for _, m := range models {
mt := reflect.TypeOf(m)
kind := strings.ToUpper(mt.Name())
db = r.session(db)
q := db.Table(kind)
q = q.Select("MAX(ID) id")
cursor, err := q.Rows()
if err != nil || !cursor.Next() {
r.add(db, kind, uint(0))
continue
}
id := int64(0)
err = cursor.Scan(&id)
_ = cursor.Close()
if err != nil {
r.add(db, kind, uint(0))
} else {
r.add(db, kind, uint(id))
}
}
return
}

// Next returns the next primary key.
func (r *PkSequence) Next(db *gorm.DB) (id uint) {
r.mutex.Lock()
defer r.mutex.Unlock()
kind := strings.ToUpper(db.Statement.Table)
m := &model.PK{}
db = r.session(db)
err := db.First(m, "Kind", kind).Error
if err != nil {
return
}
m.LastID++
db.Save(m)
id = m.LastID
return
}

func (r *PkSequence) session(in *gorm.DB) (out *gorm.DB) {
out = &gorm.DB{
Config: in.Config,
}
out.Statement = &gorm.Statement{
DB: out,
ConnPool: in.Statement.ConnPool,
Context: in.Statement.Context,
Clauses: map[string]clause.Clause{},
Vars: make([]interface{}, 0, 8),
}
return
}

func (r *PkSequence) add(db *gorm.DB, kind string, id uint) {
m := &model.PK{Kind: kind}
db = r.session(db)
err := db.First(m).Error
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
panic(err)
}
}
if m.LastID >= id {
return
}
m.LastID = id
err = db.Save(m).Error
if err != nil {
panic(err)
}
}

// assignPk assigns PK as needed.
func assignPk(db *gorm.DB) {
statement := db.Statement
schema := statement.Schema
if schema == nil {
return
}
switch statement.ReflectValue.Kind() {
case reflect.Slice,
reflect.Array:
for i := 0; i < statement.ReflectValue.Len(); i++ {
for _, f := range schema.Fields {
if f.Name != "ID" {
continue
}
_, isZero := f.ValueOf(
statement.Context,
statement.ReflectValue.Index(i))
if isZero {
id := PK.Next(db)
_ = f.Set(
statement.Context,
statement.ReflectValue.Index(i),
id)

}
break
}
}
case reflect.Struct:
for _, f := range schema.Fields {
if f.Name != "ID" {
continue
}
_, isZero := f.ValueOf(
statement.Context,
statement.ReflectValue)
if isZero {
id := PK.Next(db)
_ = f.Set(
statement.Context,
statement.ReflectValue,
id)

}
break
}
default:
log.Info("[WARN] assignPk: unknown kind.")
}
}
11 changes: 10 additions & 1 deletion database/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,16 @@ func Open(enforceFKs bool) (db *gorm.DB, err error) {
return
}
sqlDB.SetMaxOpenConns(1)
err = db.AutoMigrate(model.Setting{})
err = db.AutoMigrate(model.Setting{}, model.PK{})
if err != nil {
err = liberr.Wrap(err)
return
}
err = PK.Load(db, []any{model.Setting{}})
if err != nil {
return
}
err = db.Callback().Create().Before("gorm:before_create").Register("assign-pk", assignPk)
if err != nil {
err = liberr.Wrap(err)
return
Expand Down
4 changes: 4 additions & 0 deletions migration/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ func Migrate(migrations []Migration) (err error) {
if err != nil {
return
}
err = database.PK.Load(db, m.Models())
if err != nil {
return
}
err = setVersion(db, ver)
if err != nil {
return
Expand Down
2 changes: 2 additions & 0 deletions migration/pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
v12 "github.com/konveyor/tackle2-hub/migration/v12"
v13 "github.com/konveyor/tackle2-hub/migration/v13"
v14 "github.com/konveyor/tackle2-hub/migration/v14"
v15 "github.com/konveyor/tackle2-hub/migration/v15"
v2 "github.com/konveyor/tackle2-hub/migration/v2"
v3 "github.com/konveyor/tackle2-hub/migration/v3"
v4 "github.com/konveyor/tackle2-hub/migration/v4"
Expand Down Expand Up @@ -56,5 +57,6 @@ func All() []Migration {
v12.Migration{},
v13.Migration{},
v14.Migration{},
v15.Migration{},
}
}
20 changes: 20 additions & 0 deletions migration/v15/migrate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package v15

import (
"github.com/jortel/go-utils/logr"
"github.com/konveyor/tackle2-hub/migration/v15/model"
"gorm.io/gorm"
)

var log = logr.WithName("migration|v14")

type Migration struct{}

func (r Migration) Apply(db *gorm.DB) (err error) {
err = db.AutoMigrate(r.Models()...)
return
}

func (r Migration) Models() []interface{} {
return model.All()
}
157 changes: 157 additions & 0 deletions migration/v15/model/analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package model

import "gorm.io/gorm"

// Analysis report.
type Analysis struct {
Model
Effort int
Commit string
Archived bool
Summary JSON `gorm:"type:json"`
Issues []Issue `gorm:"constraint:OnDelete:CASCADE"`
Dependencies []TechDependency `gorm:"constraint:OnDelete:CASCADE"`
ApplicationID uint `gorm:"index;not null"`
Application *Application
}

// TechDependency report dependency.
type TechDependency struct {
Model
Provider string `gorm:"uniqueIndex:depA"`
Name string `gorm:"uniqueIndex:depA"`
Version string `gorm:"uniqueIndex:depA"`
SHA string `gorm:"uniqueIndex:depA"`
Indirect bool
Labels JSON `gorm:"type:json"`
AnalysisID uint `gorm:"index;uniqueIndex:depA;not null"`
Analysis *Analysis
}

// Issue report issue (violation).
type Issue struct {
Model
RuleSet string `gorm:"uniqueIndex:issueA;not null"`
Rule string `gorm:"uniqueIndex:issueA;not null"`
Name string `gorm:"index"`
Description string
Category string `gorm:"index;not null"`
Incidents []Incident `gorm:"foreignKey:IssueID;constraint:OnDelete:CASCADE"`
Links JSON `gorm:"type:json"`
Facts JSON `gorm:"type:json"`
Labels JSON `gorm:"type:json"`
Effort int `gorm:"index;not null"`
AnalysisID uint `gorm:"index;uniqueIndex:issueA;not null"`
Analysis *Analysis
}

// Incident report an issue incident.
type Incident struct {
Model
File string `gorm:"index;not null"`
Line int
Message string
CodeSnip string
Facts JSON `gorm:"type:json"`
IssueID uint `gorm:"index;not null"`
Issue *Issue
}

// Link URL link.
type Link struct {
URL string `json:"url"`
Title string `json:"title,omitempty"`
}

// ArchivedIssue resource created when issues are archived.
type ArchivedIssue struct {
RuleSet string `json:"ruleSet"`
Rule string `json:"rule"`
Name string `json:"name,omitempty" yaml:",omitempty"`
Description string `json:"description,omitempty" yaml:",omitempty"`
Category string `json:"category"`
Effort int `json:"effort"`
Incidents int `json:"incidents"`
}

// RuleSet - Analysis ruleset.
type RuleSet struct {
Model
UUID *string `gorm:"uniqueIndex"`
Kind string
Name string `gorm:"uniqueIndex;not null"`
Description string
Repository JSON `gorm:"type:json"`
IdentityID *uint `gorm:"index"`
Identity *Identity
Rules []Rule `gorm:"constraint:OnDelete:CASCADE"`
DependsOn []RuleSet `gorm:"many2many:RuleSetDependencies;constraint:OnDelete:CASCADE"`
}

func (r *RuleSet) Builtin() bool {
return r.UUID != nil
}

// BeforeUpdate hook to avoid cyclic dependencies.
func (r *RuleSet) BeforeUpdate(db *gorm.DB) (err error) {
seen := make(map[uint]bool)
var nextDeps []RuleSet
var nextRuleSetIDs []uint
for _, dep := range r.DependsOn {
nextRuleSetIDs = append(nextRuleSetIDs, dep.ID)
}
for len(nextRuleSetIDs) != 0 {
result := db.Preload("DependsOn").Where("ID IN ?", nextRuleSetIDs).Find(&nextDeps)
if result.Error != nil {
err = result.Error
return
}
nextRuleSetIDs = nextRuleSetIDs[:0]
for _, nextDep := range nextDeps {
for _, dep := range nextDep.DependsOn {
if seen[dep.ID] {
continue
}
if dep.ID == r.ID {
err = DependencyCyclicError{}
return
}
seen[dep.ID] = true
nextRuleSetIDs = append(nextRuleSetIDs, dep.ID)
}
}
}

return
}

// Rule - Analysis rule.
type Rule struct {
Model
Name string
Description string
Labels JSON `gorm:"type:json"`
RuleSetID uint `gorm:"uniqueIndex:RuleA;not null"`
RuleSet *RuleSet
FileID *uint `gorm:"uniqueIndex:RuleA" ref:"file"`
File *File
}

// Target - analysis rule selector.
type Target struct {
Model
UUID *string `gorm:"uniqueIndex"`
Name string `gorm:"uniqueIndex;not null"`
Description string
Provider string
Choice bool
Labels JSON `gorm:"type:json"`
ImageID uint `gorm:"index" ref:"file"`
Image *File
RuleSetID *uint `gorm:"index"`
RuleSet *RuleSet
}

func (r *Target) Builtin() bool {
return r.UUID != nil
}
Loading

0 comments on commit 3a8f00a

Please sign in to comment.