diff --git a/db/postgres/postgres.go b/db/postgres/postgres.go index 2e5a237..90dc715 100644 --- a/db/postgres/postgres.go +++ b/db/postgres/postgres.go @@ -336,6 +336,15 @@ func (pg *PostgresDB) EditNode(ctx context.Context, user db.User, nodeID string, } func (pg *PostgresDB) AddEdgeWeightVote(ctx context.Context, user db.User, edgeID string, weight float64) error { return pg.db.Transaction(func(tx *gorm.DB) error { + edgeedit := EdgeEdit{ + EdgeID: atoi(edgeID), + UserID: atoi(user.Key), + Type: db.EdgeEditTypeVote, + Weight: weight, + } + if err := tx.Create(&edgeedit).Error; err != nil { + return err + } { // TODO(skep): should move aggregation to separate module/application edge := Edge{Model: gorm.Model{ID: atoi(edgeID)}} @@ -343,25 +352,26 @@ func (pg *PostgresDB) AddEdgeWeightVote(ctx context.Context, user db.User, edgeI return err } edits := []EdgeEdit{} - if err := tx.Where(&EdgeEdit{EdgeID: edge.ID}).Find(&edits).Error; err != nil { + query := ` + WITH RankedVotes AS ( + SELECT *, + -- Assign rank to each vote per user, most recent first + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rownumber + FROM edge_edits + WHERE edge_id = ? + ) + -- Select only the most recent vote for each user (i.e. rownumber 1) + SELECT * FROM RankedVotes WHERE rownumber = 1; + ` + if err := tx.Raw(query, edge.ID).Scan(&edits).Error; err != nil { return err } sum := db.Sum(edits, func(edit EdgeEdit) float64 { return edit.Weight }) - averageWeight := (sum + weight) / float64(len(edits)+1) - edge.Weight = averageWeight + edge.Weight = sum / float64(len(edits)) if err := tx.Save(&edge).Error; err != nil { return err } } - edgeedit := EdgeEdit{ - EdgeID: atoi(edgeID), - UserID: atoi(user.Key), - Type: db.EdgeEditTypeVote, - Weight: weight, - } - if err := tx.Create(&edgeedit).Error; err != nil { - return err - } return nil }) } diff --git a/db/postgres/postgres_integration_test.go b/db/postgres/postgres_integration_test.go index 95a3c5e..297e6b6 100644 --- a/db/postgres/postgres_integration_test.go +++ b/db/postgres/postgres_integration_test.go @@ -187,38 +187,114 @@ func TestPostgresDB_CreateEdge(t *testing.T) { func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { for _, test := range []struct { - Name string + Name string + TargetEdgeID uint + PreexistingNodes []Node + PreexistingEdges []Edge + PreexistingEdgeEdits []EdgeEdit + PreexistingUsers []User + ExpectedWeight float64 + ExpectedEdgeEdits int }{ { - Name: "good case", + Name: "three votes, all from different users", + TargetEdgeID: 88, + PreexistingUsers: []User{ + {Model: gorm.Model{ID: 111}, Username: "asdf", PasswordHash: "000", EMail: "a@b"}, + {Model: gorm.Model{ID: 222}, Username: "fasd", PasswordHash: "111", EMail: "c@d"}, + {Model: gorm.Model{ID: 333}, Username: "dfas", PasswordHash: "222", EMail: "e@f"}, + }, + PreexistingNodes: []Node{ + {Model: gorm.Model{ID: 1}, Description: db.Text{"en": "A"}}, + {Model: gorm.Model{ID: 2}, Description: db.Text{"en": "B"}}, + }, + PreexistingEdges: []Edge{ + {Model: gorm.Model{ID: 88}, FromID: 1, ToID: 2, Weight: 10}, + {Model: gorm.Model{ID: 99}, FromID: 2, ToID: 1, Weight: 5}, + }, + PreexistingEdgeEdits: []EdgeEdit{ + {EdgeID: 88, UserID: 222, Weight: 10, Type: db.EdgeEditTypeCreate}, + {EdgeID: 88, UserID: 333, Weight: 5, Type: db.EdgeEditTypeVote}, + }, + ExpectedWeight: 6.33333333333333333, // = (10.0 + 5 + 4) / 3 + ExpectedEdgeEdits: 3, // two existing ones plus the new vote from the test case + }, + { + Name: "three votes, but two from the same user (new vote unaffected)", + TargetEdgeID: 88, + PreexistingUsers: []User{ + {Model: gorm.Model{ID: 111}, Username: "asdf", PasswordHash: "000", EMail: "a@b"}, + {Model: gorm.Model{ID: 222}, Username: "fasd", PasswordHash: "111", EMail: "c@d"}, + {Model: gorm.Model{ID: 333}, Username: "dfas", PasswordHash: "222", EMail: "e@f"}, + }, + PreexistingNodes: []Node{ + {Model: gorm.Model{ID: 1}, Description: db.Text{"en": "A"}}, + {Model: gorm.Model{ID: 2}, Description: db.Text{"en": "B"}}, + }, + PreexistingEdges: []Edge{ + {Model: gorm.Model{ID: 88}, FromID: 1, ToID: 2, Weight: 10}, + {Model: gorm.Model{ID: 99}, FromID: 2, ToID: 1, Weight: 5}, + }, + PreexistingEdgeEdits: []EdgeEdit{ + {EdgeID: 88, UserID: 222, Weight: 5, Type: db.EdgeEditTypeCreate}, + {EdgeID: 88, UserID: 222, Weight: 10, Type: db.EdgeEditTypeVote}, + }, + ExpectedWeight: 7.0, // = (10.0 + 4) / 2 + ExpectedEdgeEdits: 3, // two existing ones plus the new vote from the test case, but only two are counted since only a single vote per user is taken + }, + { + Name: "three votes, but two from the same user (new vote creates duplicate)", + TargetEdgeID: 88, + PreexistingUsers: []User{ + {Model: gorm.Model{ID: 111}, Username: "asdf", PasswordHash: "000", EMail: "a@b"}, + {Model: gorm.Model{ID: 222}, Username: "fasd", PasswordHash: "111", EMail: "c@d"}, + {Model: gorm.Model{ID: 333}, Username: "dfas", PasswordHash: "222", EMail: "e@f"}, + }, + PreexistingNodes: []Node{ + {Model: gorm.Model{ID: 1}, Description: db.Text{"en": "A"}}, + {Model: gorm.Model{ID: 2}, Description: db.Text{"en": "B"}}, + }, + PreexistingEdges: []Edge{ + {Model: gorm.Model{ID: 88}, FromID: 1, ToID: 2, Weight: 10}, + {Model: gorm.Model{ID: 99}, FromID: 2, ToID: 1, Weight: 5}, + }, + PreexistingEdgeEdits: []EdgeEdit{ + {EdgeID: 88, UserID: 222, Weight: 10, Type: db.EdgeEditTypeCreate}, + {EdgeID: 88, UserID: 111, Weight: 5, Type: db.EdgeEditTypeVote}, + }, + ExpectedWeight: 7.0, + ExpectedEdgeEdits: 3, }, } { t.Run(test.Name, func(t *testing.T) { pg := setupDB(t) ctx := context.Background() assert := assert.New(t) - A := Node{Description: db.Text{"en": "A"}} - assert.NoError(pg.db.Create(&A).Error) - B := Node{Description: db.Text{"en": "B"}} - assert.NoError(pg.db.Create(&B).Error) - user := User{Username: "123", PasswordHash: "000", EMail: "a@b"} - assert.NoError(pg.db.Create(&user).Error) - edge := Edge{Model: gorm.Model{ID: 88}, From: A, To: B, Weight: 10} - assert.NoError(pg.db.Create(&edge).Error) - assert.NoError(pg.db.Create(&Edge{Model: gorm.Model{ID: 99}, From: B, To: A, Weight: 5}).Error) - existing_edits := []EdgeEdit{ - {EdgeID: edge.ID, UserID: user.ID, Weight: 10, Type: db.EdgeEditTypeCreate}, - {EdgeID: 99, UserID: user.ID, Weight: 5, Type: db.EdgeEditTypeCreate}, + for _, node := range test.PreexistingNodes { + assert.NoError(pg.db.Create(&node).Error) } - assert.NoError(pg.db.Create(&existing_edits).Error) - currentUser := db.User{Document: db.Document{Key: itoa(user.ID)}} - err := pg.AddEdgeWeightVote(ctx, currentUser, itoa(edge.ID), 4) + for _, edge := range test.PreexistingEdges { + assert.NoError(pg.db.Create(&edge).Error) + } + for _, user := range test.PreexistingUsers { + assert.NoError(pg.db.Create(&user).Error) + } + for _, edgeedit := range test.PreexistingEdgeEdits { + assert.NoError(pg.db.Create(&edgeedit).Error) + } + currentUser := db.User{Document: db.Document{Key: itoa(111)}} + err := pg.AddEdgeWeightVote(ctx, currentUser, itoa(test.TargetEdgeID), 4) assert.NoError(err) edgeedits := []EdgeEdit{} - assert.NoError(pg.db.Where(&EdgeEdit{EdgeID: edge.ID}).Find(&edgeedits).Error) - assert.Len(edgeedits, 2) + assert.NoError(pg.db.Where(&EdgeEdit{EdgeID: test.TargetEdgeID}).Find(&edgeedits).Error) + t.Log("edge edits for target edge:") + for _, edit := range edgeedits { + t.Logf("edgeID:%v userID:%v weight:%v type:%v", edit.EdgeID, edit.UserID, edit.Weight, edit.Type) + } + assert.Len(edgeedits, test.ExpectedEdgeEdits) + edge := Edge{} assert.NoError(pg.db.First(&edge).Error) - assert.Equal(7.0, edge.Weight) + assert.Equal(test.ExpectedWeight, edge.Weight) }) } }