147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
package test
|
|
|
|
import (
|
|
domain "RenewCMS/internal/domain/article"
|
|
"RenewCMS/internal/infrastructure/persistence/database"
|
|
entity "RenewCMS/internal/infrastructure/persistence/models"
|
|
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
|
"fmt"
|
|
"slices"
|
|
"testing"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
chunkedArticleCount = 1200
|
|
chunkedCreditedPool = 700
|
|
chunkedTitlePrefix = "Chunk"
|
|
)
|
|
|
|
func seedChunkedArticles(t *testing.T, db *gorm.DB, count int) {
|
|
t.Helper()
|
|
rows := make([]entity.Article, 0, count)
|
|
for i := 1; i <= count; i++ {
|
|
rows = append(rows, entity.Article{Title: fmt.Sprintf("%s %04d", chunkedTitlePrefix, i), Slug: fmt.Sprintf("chunk-%04d", i), SiteID: 1, IsOnline: true})
|
|
}
|
|
if err := db.CreateInBatches(&rows, 100).Error; err != nil {
|
|
t.Fatalf("seed articles: %v", err)
|
|
}
|
|
}
|
|
|
|
func seedChunkedCredits(t *testing.T, db *gorm.DB, articles int) []uint32 {
|
|
t.Helper()
|
|
authors := make([]entity.Author, 0, 3)
|
|
for i := 1; i <= 3; i++ {
|
|
authors = append(authors, entity.Author{Name: fmt.Sprintf("Author %d", i), Slug: fmt.Sprintf("author-%d", i), SocialLinks: "[]"})
|
|
}
|
|
if err := db.Create(&authors).Error; err != nil {
|
|
t.Fatalf("seed authors: %v", err)
|
|
}
|
|
order := []uint32{authors[2].ID, authors[0].ID, authors[1].ID}
|
|
credits := make([]entity.ArticleAuthor, 0, articles*3)
|
|
for article := 1; article <= articles; article++ {
|
|
for _, position := range []int{2, 0, 1} {
|
|
credits = append(credits, entity.ArticleAuthor{ArticleID: uint32(article), AuthorID: order[position], Position: position})
|
|
}
|
|
}
|
|
if err := db.CreateInBatches(&credits, 100).Error; err != nil {
|
|
t.Fatalf("seed credits: %v", err)
|
|
}
|
|
return []uint32{order[0], order[1], order[2]}
|
|
}
|
|
|
|
func chunkedFilters(size int) domain.Filters {
|
|
return domain.Filters{Q: chunkedTitlePrefix, Page: 1, Size: size, Sort: domain.Sort{Field: domain.SortByID, Direction: domain.SortAsc}}
|
|
}
|
|
|
|
func runChunkedLookups(t *testing.T, db *gorm.DB) {
|
|
seedChunkedArticles(t, db, chunkedArticleCount)
|
|
repo := persistence.NewArticleRepository(db)
|
|
|
|
t.Run("A page-sized lookup keeps every row once and in order", func(t *testing.T) {
|
|
filters := chunkedFilters(0)
|
|
filters.Unpaged = true
|
|
page, err := repo.FindPage(filters)
|
|
if err != nil || page.Total != chunkedArticleCount || len(page.Items) != chunkedArticleCount {
|
|
t.Fatalf("total %d items %d err %v", page.Total, len(page.Items), err)
|
|
}
|
|
ids := make([]uint32, 0, len(page.Items))
|
|
for _, item := range page.Items {
|
|
ids = append(ids, item.ID)
|
|
}
|
|
if !slices.IsSorted(ids) || len(slices.Compact(slices.Clone(ids))) != chunkedArticleCount {
|
|
t.Fatalf("ids are not unique and ascending")
|
|
}
|
|
})
|
|
|
|
t.Run("Batch boundaries", func(t *testing.T) {
|
|
for _, n := range []int{0, 1, 499, 500, 501, 1000, 1001} {
|
|
page, err := repo.FindPage(chunkedFilters(n))
|
|
if err != nil || len(page.Items) != n || page.Total != chunkedArticleCount {
|
|
t.Errorf("n=%d: items %d total %d err %v", n, len(page.Items), page.Total, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Credits keep their order across batches", func(t *testing.T) {
|
|
expected := seedChunkedCredits(t, db, chunkedCreditedPool)
|
|
page, err := repo.FindPage(chunkedFilters(chunkedCreditedPool))
|
|
if err != nil || len(page.Items) != chunkedCreditedPool {
|
|
t.Fatalf("items %d err %v", len(page.Items), err)
|
|
}
|
|
for _, item := range page.Items {
|
|
got := make([]uint32, 0, len(item.Authors))
|
|
for _, byline := range item.Authors {
|
|
got = append(got, byline.ID)
|
|
}
|
|
if !slices.Equal(got, expected) {
|
|
t.Fatalf("article %d credits %v, expected %v", item.ID, got, expected)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Soft-deleted rows follow the existing rule", func(t *testing.T) {
|
|
if err := db.Where("id IN ?", []uint32{1, 600, 1200}).Delete(&entity.Article{}).Error; err != nil {
|
|
t.Fatalf("soft delete: %v", err)
|
|
}
|
|
filters := chunkedFilters(0)
|
|
filters.Unpaged = true
|
|
page, err := repo.FindPage(filters)
|
|
if err != nil || len(page.Items) != chunkedArticleCount-3 || page.Total != chunkedArticleCount-3 {
|
|
t.Fatalf("items %d total %d err %v", len(page.Items), page.Total, err)
|
|
}
|
|
for _, item := range page.Items {
|
|
if item.ID == 1 || item.ID == 600 || item.ID == 1200 {
|
|
t.Fatalf("deleted article %d returned", item.ID)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Ids that no longer exist are absent", func(t *testing.T) {
|
|
if err := db.Where("id BETWEEN ? AND ?", 101, 200).Delete(&entity.Article{}).Error; err != nil {
|
|
t.Fatalf("soft delete: %v", err)
|
|
}
|
|
page, err := repo.FindPage(chunkedFilters(600))
|
|
if err != nil || len(page.Items) != 600 || page.Total != chunkedArticleCount-103 {
|
|
t.Fatalf("items %d total %d err %v", len(page.Items), page.Total, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestChunkedLookups(t *testing.T) {
|
|
db := openUnmigratedSQLite(t)
|
|
if err := database.Migrate(db, migrationPublicURL); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
runChunkedLookups(t, db)
|
|
}
|
|
|
|
func TestMySQLChunkedLookups(t *testing.T) {
|
|
runChunkedLookups(t, openContractMySQL(t))
|
|
}
|
|
|
|
func TestPostgresChunkedLookups(t *testing.T) {
|
|
runChunkedLookups(t, openContractPostgres(t))
|
|
}
|