From e0b5ac4e2e347d6dfa16c4021b25efaf7a08eee0 Mon Sep 17 00:00:00 2001 From: skep Date: Sun, 9 Jun 2024 13:56:51 +0000 Subject: [PATCH 1/4] WIP: test --- db/postgres/postgres_integration_test.go | 66 +++++++++++++++++------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/db/postgres/postgres_integration_test.go b/db/postgres/postgres_integration_test.go index 95a3c5e..712565c 100644 --- a/db/postgres/postgres_integration_test.go +++ b/db/postgres/postgres_integration_test.go @@ -187,38 +187,64 @@ 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 + PreexistingNodeEdits []NodeEdit + PreexistingEdgeEdits []EdgeEdit + PreexistingUsers []User + ExpectedWeight float64 + ExpectedEdgeEdits int }{ { - Name: "good case", + Name: "two votes 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"}, + }, + 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: 111, Weight: 10, Type: db.EdgeEditTypeCreate}, + {EdgeID: 88, UserID: 222, Weight: 5, Type: db.EdgeEditTypeVote}, + }, + ExpectedWeight: 6.33333333333333333, + ExpectedEdgeEdits: 2, }, } { 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) + } + 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) } - 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) + 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) + 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) }) } } From 2d14484b9e642fec614efc535603789da848514b Mon Sep 17 00:00:00 2001 From: skep Date: Sun, 9 Jun 2024 14:08:15 +0000 Subject: [PATCH 2/4] TDD: wrote a failing test --- db/postgres/postgres_integration_test.go | 39 ++++++++++++++++++++---- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/db/postgres/postgres_integration_test.go b/db/postgres/postgres_integration_test.go index 712565c..b09dfc8 100644 --- a/db/postgres/postgres_integration_test.go +++ b/db/postgres/postgres_integration_test.go @@ -191,18 +191,18 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { TargetEdgeID uint PreexistingNodes []Node PreexistingEdges []Edge - PreexistingNodeEdits []NodeEdit PreexistingEdgeEdits []EdgeEdit PreexistingUsers []User ExpectedWeight float64 ExpectedEdgeEdits int }{ { - Name: "two votes from different users", + 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"}}, @@ -213,11 +213,34 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { {Model: gorm.Model{ID: 99}, FromID: 2, ToID: 1, Weight: 5}, }, PreexistingEdgeEdits: []EdgeEdit{ - {EdgeID: 88, UserID: 111, Weight: 10, Type: db.EdgeEditTypeCreate}, - {EdgeID: 88, UserID: 222, Weight: 5, Type: db.EdgeEditTypeVote}, + {EdgeID: 88, UserID: 222, Weight: 10, Type: db.EdgeEditTypeCreate}, + {EdgeID: 88, UserID: 333, Weight: 5, Type: db.EdgeEditTypeVote}, }, - ExpectedWeight: 6.33333333333333333, - ExpectedEdgeEdits: 2, + 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", + 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, // = (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 }, } { t.Run(test.Name, func(t *testing.T) { @@ -241,6 +264,10 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { assert.NoError(err) edgeedits := []EdgeEdit{} 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("%v %v %v", edit.EdgeID, edit.Weight, edit.Type) + } assert.Len(edgeedits, test.ExpectedEdgeEdits) edge := Edge{} assert.NoError(pg.db.First(&edge).Error) From ef6aaeff7fa0a3afcd60761e21ad18a63d0dfda1 Mon Sep 17 00:00:00 2001 From: skep Date: Sun, 9 Jun 2024 14:43:57 +0000 Subject: [PATCH 3/4] fix: it --- db/postgres/postgres.go | 31 +++++++++++++++--------- db/postgres/postgres_integration_test.go | 31 +++++++++++++++++++++--- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/db/postgres/postgres.go b/db/postgres/postgres.go index 2e5a237..aeb0a3f 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,23 @@ 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 *, + ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn + FROM edge_edits + WHERE edge_id = ? + ) + SELECT * FROM RankedVotes WHERE rn = 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 b09dfc8..297e6b6 100644 --- a/db/postgres/postgres_integration_test.go +++ b/db/postgres/postgres_integration_test.go @@ -220,7 +220,7 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { ExpectedEdgeEdits: 3, // two existing ones plus the new vote from the test case }, { - Name: "three votes, but two from the same user", + 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"}, @@ -236,12 +236,35 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { {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}, + {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) @@ -266,7 +289,7 @@ func TestPostgresDB_AddEdgeWeightVote(t *testing.T) { 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("%v %v %v", edit.EdgeID, edit.Weight, edit.Type) + 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{} From dac2e85d6923beee07d4a4e2e5128fd1e317a544 Mon Sep 17 00:00:00 2001 From: skep Date: Sun, 9 Jun 2024 14:46:57 +0000 Subject: [PATCH 4/4] doc: comment sql code --- db/postgres/postgres.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/db/postgres/postgres.go b/db/postgres/postgres.go index aeb0a3f..90dc715 100644 --- a/db/postgres/postgres.go +++ b/db/postgres/postgres.go @@ -355,11 +355,14 @@ func (pg *PostgresDB) AddEdgeWeightVote(ctx context.Context, user db.User, edgeI query := ` WITH RankedVotes AS ( SELECT *, - ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as rn + -- 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 * FROM RankedVotes WHERE rn = 1; ` + -- 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 }