Commit cdadf38b authored by Amos Wenger's avatar Amos Wenger

Finish implementing squash

parent 12ceaf03
Pipeline #10984 passed with stage
in 47 seconds
package hades_test
import (
"testing"
"crawshaw.io/sqlite"
"github.com/go-xorm/builder"
"github.com/itchio/hades"
"github.com/itchio/wharf/wtest"
"github.com/stretchr/testify/assert"
)
func Test_BelongsTo(t *testing.T) {
type Fate struct {
ID int64
Desc string
}
type Human struct {
ID int64
FateID int64
Fate *Fate `hades:"ignore"`
}
type Joke struct {
ID string
HumanID int64
Human *Human `hades:"ignore"`
}
models := []interface{}{&Human{}, &Fate{}, &Joke{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
someFate := &Fate{
ID: 123,
Desc: "Consumer-grade flamethrowers",
}
t.Log("Saving one fate")
wtest.Must(t, c.SaveOne(conn, someFate))
lea := &Human{
ID: 3,
FateID: someFate.ID,
}
t.Log("Saving one human")
wtest.Must(t, c.SaveOne(conn, lea))
t.Log("Preloading lea")
c.Preload(conn, &hades.PreloadParams{
Record: lea,
Fields: []hades.PreloadField{
{Name: "Fate"},
},
})
assert.NotNil(t, lea.Fate)
assert.EqualValues(t, someFate.Desc, lea.Fate.Desc)
})
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
lea := &Human{
ID: 3,
Fate: &Fate{
ID: 421,
Desc: "Book authorship",
},
}
c.Save(conn, &hades.SaveParams{
Record: lea,
Assocs: []string{"Fate"},
})
fate := &Fate{}
wtest.Must(t, c.SelectOne(conn, fate, builder.Eq{"id": 421}))
assert.EqualValues(t, "Book authorship", fate.Desc)
})
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
fate := &Fate{
ID: 3,
Desc: "Space rodeo",
}
wtest.Must(t, c.SaveOne(conn, fate))
human := &Human{
ID: 6,
FateID: 3,
}
wtest.Must(t, c.SaveOne(conn, human))
joke := &Joke{
ID: "neuf",
HumanID: 6,
}
wtest.Must(t, c.SaveOne(conn, joke))
c.Preload(conn, &hades.PreloadParams{
Record: joke,
Fields: []hades.PreloadField{
{Name: "Human"},
{Name: "Human.Fate"},
},
})
assert.NotNil(t, joke.Human)
assert.NotNil(t, joke.Human.Fate)
assert.EqualValues(t, "Space rodeo", joke.Human.Fate.Desc)
})
}
......@@ -10,14 +10,14 @@ import (
type ChangedFields map[*StructField]interface{}
func DiffRecord(x, y interface{}, scope *Scope) (ChangedFields, error) {
if x == nil || y == nil {
func DiffRecord(freshRecord, cachedRecord interface{}, scope *Scope) (ChangedFields, error) {
if freshRecord == nil || cachedRecord == nil {
return nil, errors.New("DiffRecord: arguments must not be nil")
}
// v1 is the fresh record (being saved)
v1 := reflect.ValueOf(x)
v1 := reflect.ValueOf(freshRecord)
// v2 is the cached record (in DB)
v2 := reflect.ValueOf(y)
v2 := reflect.ValueOf(cachedRecord)
if v1.Type() != v2.Type() {
return nil, errors.New("DiffRecord: arguments are not the same type")
}
......@@ -29,26 +29,28 @@ func DiffRecord(x, y interface{}, scope *Scope) (ChangedFields, error) {
ms := scope.GetModelStruct()
var res ChangedFields
for i, n := 0, v1.NumField(); i < n; i++ {
f := typ.Field(i)
fieldName := f.Name
sf, ok := ms.StructFieldsByName[fieldName]
if !ok {
// not listed as a field? ignore
continue
var processField func(sf *StructField, v1 reflect.Value, v2 reflect.Value) error
processField = func(sf *StructField, v1 reflect.Value, v2 reflect.Value) error {
v1f := v1.FieldByName(sf.Name)
v2f := v2.FieldByName(sf.Name)
if sf.IsSquashed {
for _, nsf := range sf.SquashedFields {
err := processField(nsf, v1f, v2f)
if err != nil {
return err
}
}
}
if !sf.IsNormal {
continue
return nil
}
v1f := v1.Field(i)
v2f := v2.Field(i)
iseq, err := iseq(sf, v1f, v2f)
if err != nil {
return res, err
return err
}
if !iseq {
......@@ -57,6 +59,14 @@ func DiffRecord(x, y interface{}, scope *Scope) (ChangedFields, error) {
}
res[sf] = v1f.Interface()
}
return nil
}
for _, sf := range ms.StructFields {
err := processField(sf, v1, v2)
if err != nil {
return res, nil
}
}
return res, nil
......
This diff is collapsed.
......@@ -10,8 +10,103 @@ import (
"crawshaw.io/sqlite"
"github.com/itchio/hades"
"github.com/itchio/wharf/wtest"
)
func Test_HasMany(t *testing.T) {
type Quality struct {
ID int64
ProgrammerID int64
Label string
}
type Programmer struct {
ID int64
Qualities []*Quality
}
models := []interface{}{&Quality{}, &Programmer{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
assertCount := func(model interface{}, expectedCount int64) {
t.Helper()
var count int64
count, err := c.Count(conn, model, builder.NewCond())
wtest.Must(t, err)
assert.EqualValues(t, expectedCount, count)
}
p1 := &Programmer{
ID: 3,
Qualities: []*Quality{
{ID: 9, Label: "Inspiration"},
{ID: 10, Label: "Creativity"},
{ID: 11, Label: "Ability to not repeat oneself"},
},
}
wtest.Must(t, c.Save(conn, &hades.SaveParams{Record: p1}))
assertCount(&Programmer{}, 1)
assertCount(&Quality{}, 3)
p1.Qualities[2].Label = "Inspiration again"
wtest.Must(t, c.Save(conn, &hades.SaveParams{Record: p1}))
assertCount(&Programmer{}, 1)
assertCount(&Quality{}, 3)
{
q := &Quality{}
wtest.Must(t, c.SelectOne(conn, q, builder.Eq{"id": 11}))
assert.EqualValues(t, "Inspiration again", q.Label)
}
p2 := &Programmer{
ID: 8,
Qualities: []*Quality{
{ID: 40, Label: "Peace"},
{ID: 41, Label: "Serenity"},
},
}
programmers := []*Programmer{p1, p2}
wtest.Must(t, c.Save(conn, &hades.SaveParams{Record: programmers}))
assertCount(&Programmer{}, 2)
assertCount(&Quality{}, 5)
p1bis := &Programmer{ID: 3}
pp := &hades.PreloadParams{
Record: p1bis,
Fields: []hades.PreloadField{
{Name: "Qualities"},
},
}
wtest.Must(t, c.Preload(conn, pp))
assert.EqualValues(t, 3, len(p1bis.Qualities), "preload has_many")
wtest.Must(t, c.Preload(conn, pp))
assert.EqualValues(t, 3, len(p1bis.Qualities), "preload replaces, doesn't append")
pp.Fields[0] = hades.PreloadField{
Name: "Qualities",
Search: hades.Search().OrderBy("id asc"),
}
wtest.Must(t, c.Preload(conn, pp))
assert.EqualValues(t, "Inspiration", p1bis.Qualities[0].Label, "orders by (asc)")
pp.Fields[0] = hades.PreloadField{
Name: "Qualities",
Search: hades.Search().OrderBy("id desc"),
}
wtest.Must(t, c.Preload(conn, pp))
assert.EqualValues(t, "Inspiration again", p1bis.Qualities[0].Label, "orders by (desc)")
// no fields
assert.Error(t, c.Preload(conn, &hades.PreloadParams{Record: p1bis}))
// not a model
assert.Error(t, c.Preload(conn, &hades.PreloadParams{Record: 42, Fields: pp.Fields}))
// non-existent relation
assert.Error(t, c.Preload(conn, &hades.PreloadParams{Record: p1bis, Fields: []hades.PreloadField{{Name: "Woops"}}}))
})
}
func Test_HasManyThorough(t *testing.T) {
dbpool, err := sqlite.Open("file:memory:?mode=memory", 0, 10)
ordie(err)
......
package hades_test
import (
"testing"
"crawshaw.io/sqlite"
"github.com/go-xorm/builder"
"github.com/itchio/hades"
"github.com/itchio/wharf/wtest"
"github.com/stretchr/testify/assert"
)
func Test_HasOne(t *testing.T) {
type Drawback struct {
ID int64
Comment string
SpecialtyID string
}
type Specialty struct {
ID string
CountryID int64
Drawback *Drawback
}
type Country struct {
ID int64
Desc string
Specialty *Specialty
}
models := []interface{}{&Country{}, &Specialty{}, &Drawback{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
country := &Country{
ID: 324,
Desc: "Shmance",
Specialty: &Specialty{
ID: "complain",
Drawback: &Drawback{
ID: 1249,
Comment: "bitterness",
},
},
}
assertCount := func(model interface{}, expectedCount int64) {
t.Helper()
var count int64
count, err := c.Count(conn, model, builder.NewCond())
wtest.Must(t, err)
assert.EqualValues(t, expectedCount, count)
}
wtest.Must(t, c.Save(conn, &hades.SaveParams{Record: country, Assocs: []string{"Specialty"}}))
assertCount(&Country{}, 0)
assertCount(&Specialty{}, 1)
assertCount(&Drawback{}, 1)
wtest.Must(t, c.Save(conn, &hades.SaveParams{Record: country}))
assertCount(&Country{}, 1)
assertCount(&Specialty{}, 1)
assertCount(&Drawback{}, 1)
var countries []*Country
for i := 0; i < 4; i++ {
country := &Country{}
wtest.Must(t, c.SelectOne(conn, country, builder.Eq{"id": 324}))
countries = append(countries, country)
}
wtest.Must(t, c.Preload(conn, &hades.PreloadParams{
Record: countries,
Fields: []hades.PreloadField{
{Name: "Specialty"},
{Name: "Specialty.Drawback"},
},
}))
})
}
......@@ -19,11 +19,24 @@ func (scope *Scope) ToEq(rec reflect.Value) builder.Eq {
}
eq := make(builder.Eq)
for _, sf := range scope.GetModelStruct().StructFields {
var processField func(sf *StructField, val reflect.Value)
processField = func(sf *StructField, val reflect.Value) {
field := val.FieldByName(sf.Name)
if sf.IsSquashed {
for _, nsf := range sf.SquashedFields {
processField(nsf, field)
}
}
if !sf.IsNormal {
continue
return
}
eq[sf.DBName] = DBValue(recEl.FieldByName(sf.Name).Interface())
eq[sf.DBName] = DBValue(field.Interface())
}
for _, sf := range scope.GetModelStruct().StructFields {
processField(sf, recEl)
}
return eq
}
......
......@@ -9,8 +9,180 @@ import (
"crawshaw.io/sqlite"
"github.com/itchio/hades"
"github.com/itchio/wharf/wtest"
)
type Language struct {
ID int64
Words []*Word `hades:"many2many:language_words"`
}
type Word struct {
ID string
Comment string
Languages []*Language `hades:"many2many:language_words"`
}
type LanguageWord struct {
LanguageID int64 `hades:"primary_key;auto_increment:false"`
WordID string `hades:"primary_key;auto_increment:false"`
}
func Test_ManyToMany(t *testing.T) {
models := []interface{}{&Language{}, &Word{}, &LanguageWord{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
fr := &Language{
ID: 123,
Words: []*Word{
{ID: "Plume"},
{ID: "Week-end"},
},
}
t.Logf("saving just fr")
wtest.Must(t, c.Save(conn, &hades.SaveParams{
Record: fr,
}))
assertCount := func(model interface{}, expectedCount int64) {
t.Helper()
var count int64
count, err := c.Count(conn, model, builder.NewCond())
wtest.Must(t, err)
assert.EqualValues(t, expectedCount, count)
}
assertCount(&Language{}, 1)
assertCount(&Word{}, 2)
assertCount(&LanguageWord{}, 2)
en := &Language{
ID: 456,
Words: []*Word{
{ID: "Plume"},
{ID: "Week-end"},
},
}
t.Logf("saving fr+en")
wtest.Must(t, c.Save(conn, &hades.SaveParams{
Record: []*Language{fr, en},
}))
assertCount(&Language{}, 2)
assertCount(&Word{}, 2)
assertCount(&LanguageWord{}, 4)
t.Logf("saving without culling ('add' words to english)")
en.Words = []*Word{
{ID: "Wreck"},
{ID: "Nervous"},
}
wtest.Must(t, c.Save(conn, &hades.SaveParams{
Record: []*Language{en},
DontCull: []interface{}{&LanguageWord{}},
}))
assertCount(&Language{}, 2)
assertCount(&Word{}, 4)
assertCount(&LanguageWord{}, 6)
t.Logf("replacing all english words")
wtest.Must(t, c.Save(conn, &hades.SaveParams{
Record: []*Language{en},
}))
assertCount(&Language{}, 2)
assertCount(&Word{}, 4)
assertCount(&LanguageWord{}, 4)
t.Logf("adding commentary")
en.Words[0].Comment = "punk band reference"
wtest.Must(t, c.Save(conn, &hades.SaveParams{
Record: []*Language{en},
}))
assertCount(&Language{}, 2)
assertCount(&Word{}, 4)
assertCount(&LanguageWord{}, 4)
{
w := &Word{}
wtest.Must(t, c.SelectOne(conn, w, builder.Eq{"id": "Wreck"}))
assert.EqualValues(t, "punk band reference", w.Comment)
}
langs := []*Language{
{ID: fr.ID},
{ID: en.ID},
}
err := c.Preload(conn, &hades.PreloadParams{
Record: langs,
Fields: []hades.PreloadField{
{Name: "Words"},
},
})
// many_to_many preload is not implemented
assert.Error(t, err)
})
}
type Profile struct {
ID int64
ProfileGames []*ProfileGame
}
type Game struct {
ID int64
Title string
}
type ProfileGame struct {
ProfileID int64 `hades:"primary_key;auto_increment:false"`
Profile *Profile
GameID int64 `hades:"primary_key;auto_increment:false"`
Game *Game
Order int64
}
func Test_ManyToManyRevenge(t *testing.T) {
models := []interface{}{&Profile{}, &ProfileGame{}, &Game{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
makeProfile := func() *Profile {
return &Profile{
ID: 389,
ProfileGames: []*ProfileGame{
{
Order: 1,
Game: &Game{
ID: 58372,
Title: "First offensive",
},
},
{
Order: 5,
Game: &Game{
ID: 235971,
Title: "Seconds until midnight",
},
},
{
Order: 7,
Game: &Game{
ID: 10598,
Title: "Three was company",
},
},
},
}
}
p := makeProfile()
c.Save(conn, &hades.SaveParams{
Record: p,
})
})
}
type Piece struct {
ID int64
Authors []*Author `hades:"many2many:piece_authors"`
......
......@@ -66,7 +66,7 @@ func (n *Node) Add(pf PreloadField) {
}
}
func (c *Context) Preload(db *sqlite.Conn, params *PreloadParams) error {
func (c *Context) Preload(conn *sqlite.Conn, params *PreloadParams) error {
rec := params.Record
if len(params.Fields) == 0 {
return errors.New("Preload expects a non-empty list in Fields")
......@@ -136,7 +136,7 @@ func (c *Context) Preload(db *sqlite.Conn, params *PreloadParams) error {
}
var err error
freshAddr, err = c.fetchPagedByPK(db, cri.Relationship.ForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
freshAddr, err = c.fetchPagedByPK(conn, cri.Relationship.ForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
if err != nil {
return errors.Wrap(err, "fetching has_many records (paginated)")
}
......@@ -169,7 +169,7 @@ func (c *Context) Preload(db *sqlite.Conn, params *PreloadParams) error {
}
var err error
freshAddr, err = c.fetchPagedByPK(db, cri.Relationship.ForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
freshAddr, err = c.fetchPagedByPK(conn, cri.Relationship.ForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
if err != nil {
return errors.Wrap(err, "fetching has_one records (paginated)")
}
......@@ -197,7 +197,7 @@ func (c *Context) Preload(db *sqlite.Conn, params *PreloadParams) error {
}
var err error
freshAddr, err = c.fetchPagedByPK(db, cri.Relationship.AssociationForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
freshAddr, err = c.fetchPagedByPK(conn, cri.Relationship.AssociationForeignDBNames[0], keys, reflect.SliceOf(cri.Type), cvt.Search)
if err != nil {
return errors.Wrap(err, "fetching belongs_to records (paginated)")
}
......
package hades_test
import (
"testing"
"crawshaw.io/sqlite"
"github.com/itchio/hades"
"github.com/itchio/wharf/wtest"
)
func Test_PreloadEdgeCases(t *testing.T) {
type Bar struct {
ID int64
}
type Foo struct {
ID int64
BarID int64
Bar *Bar
}
models := []interface{}{&Foo{}, &Bar{}}
withContext(t, models, func(conn *sqlite.Conn, c *hades.Context) {
// non-existent Bar
f := &Foo{ID: 1, BarID: 999}
wtest.Must(t, c.Preload(conn, &hades.PreloadParams{
Record: f,
Fields: []hades.PreloadField{
{Name: "Bar"},
},
}))
// empty slice
var foos []*Foo
wtest.Must(t, c.Preload(conn, &hades.PreloadParams{
Record: foos,
Fields: []hades.PreloadField{
{Name: "Bar"},
},
}))
})
}
......@@ -36,12 +36,20 @@ func (s *SearchParams) Apply(sql string) string {
sql = fmt.Sprintf("%s ORDER BY %s", sql, strings.Join(s.orders, ", "))
}
if s.offset != nil {
sql = fmt.Sprintf("%s OFFSET %d", sql, *s.offset)
}
if s.limit != nil {
sql = fmt.Sprintf("%s LIMIT %d", sql, *s.limit)
if s.limit != nil || s.offset != nil {
var limit int64 = -1
if s.limit != nil {
limit = *s.limit
}
// offset must appear without limit,
// and a negative limit means no limit.
// see https://www.sqlite.org/lang_select.html#limitoffset
sql = fmt.Sprintf("%s LIMIT %d", sql, limit)
if s.offset != nil {
sql = fmt.Sprintf("%s OFFSET %d", sql, *s.offset)
}
}
}