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