Files
RenewCMS/test/article_chunked_lookups_test.go
T

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))
}