Skip to content

Commit ec599f0

Browse files
authored
Merge pull request #30 from 2222-42/feat/issue-#27
refactor: introduce CSVRecordIterator for paginated and optimized CSV repository operations.
2 parents 6c9d419 + dbee4f4 commit ec599f0

File tree

11 files changed

+645
-57
lines changed

11 files changed

+645
-57
lines changed

cmd/biblog/main.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ func main() {
4848
updateReviewGoals := updateReviewCmd.String("goals", "", "New goals for reading (optional)")
4949
updateReviewSummary := updateReviewCmd.String("summary", "", "New summary of the review (optional)")
5050

51+
// List Flags
52+
listLimit := listCmd.Int("limit", 100, "Maximum number of items to display (default: 100, 0 for all)")
53+
listOffset := listCmd.Int("offset", 0, "Number of items to skip (default: 0)")
54+
5155
if len(os.Args) < 2 {
5256
fmt.Println("expected 'add-class', 'add-bib', 'add-review', 'update-review' or 'list' subcommands")
5357
os.Exit(1)
@@ -186,7 +190,8 @@ func main() {
186190

187191
case "list":
188192
_ = listCmd.Parse(os.Args[2:])
189-
bibs, err := app.BibService.ListBibliographies()
193+
// Use user-specified limit and offset, or defaults
194+
bibs, err := app.BibService.ListBibliographies(*listLimit, *listOffset)
190195
if err != nil {
191196
fmt.Printf("Error listing bibliographies: %v\n", err)
192197
os.Exit(1)
@@ -195,6 +200,9 @@ func main() {
195200
for _, b := range bibs {
196201
fmt.Printf("[%s] %s by %s (BibIndex: %s)\n", b.Type, b.Title, b.Author, b.BibIndex)
197202
}
203+
if len(bibs) == *listLimit && *listLimit > 0 {
204+
fmt.Printf("\nShowing %d items (use --limit and --offset to see more)\n", len(bibs))
205+
}
198206

199207
default:
200208
fmt.Println("expected 'add-class', 'add-bib', 'add-review', 'update-review' or 'list' subcommands")

internal/domain/repository.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,22 @@ package domain
33
// BibliographyRepository defines the interface for persistence.
44
type BibliographyRepository interface {
55
Save(bibliography *Bibliography) error
6-
FindAll() ([]*Bibliography, error)
6+
FindAll(limit, offset int) ([]*Bibliography, error)
77
FindByID(id BibliographyID) (*Bibliography, error)
88
FindByBibIndex(bibIndex string) (*Bibliography, error)
99
}
1010

1111
// ClassificationRepository defines the interface for persistence.
1212
type ClassificationRepository interface {
1313
Save(classification *Classification) error
14-
FindAll() ([]*Classification, error)
14+
FindAll(limit, offset int) ([]*Classification, error)
1515
FindByCodeNum(codeNum int) (*Classification, error)
1616
}
1717

1818
// ReviewRepository defines the interface for persistence.
1919
type ReviewRepository interface {
2020
Save(review *Review) error
21-
FindAll() ([]*Review, error)
21+
FindAll(limit, offset int) ([]*Review, error)
2222
FindByID(id ReviewID) (*Review, error)
2323
FindByBookID(bookID BibliographyID) ([]*Review, error)
2424
}

internal/infrastructure/bibliography_repository.go

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,47 @@ func NewCSVBibliographyRepository(filePath string) *CSVBibliographyRepository {
7474
// without any locking mechanism. Acceptable for single-user CLI usage, but consider file locking
7575
// or using a database with proper transaction support for production use.
7676
func (r *CSVBibliographyRepository) Save(b *domain.Bibliography) error {
77-
all, err := r.FindAll()
77+
records, err := ReadCSV(r.FilePath)
7878
if err != nil {
7979
return err
8080
}
8181

82+
// Skip header
83+
if len(records) > 0 {
84+
records = records[1:]
85+
}
86+
87+
iter := NewCSVRecordIterator(records, 0, 0)
88+
var all []*domain.Bibliography
89+
90+
for iter.Next() {
91+
record := iter.Record()
92+
if len(record) < 9 {
93+
continue
94+
}
95+
bibRecord := &BibliographyRecord{
96+
ID: record[0],
97+
BibIndex: record[1],
98+
Code: record[2],
99+
Type: record[3],
100+
Title: record[4],
101+
Author: record[5],
102+
Publisher: record[6],
103+
ISBN: record[7],
104+
PublishedDate: record[8],
105+
}
106+
bib, err := recordToBibliography(bibRecord)
107+
if err != nil {
108+
slog.Error("Failed to convert bibliography record", "err", err)
109+
continue
110+
}
111+
all = append(all, bib)
112+
}
113+
114+
if iter.Err() != nil {
115+
return iter.Err()
116+
}
117+
82118
updated := false
83119
for i, existing := range all {
84120
if existing.ID == b.ID {
@@ -94,19 +130,22 @@ func (r *CSVBibliographyRepository) Save(b *domain.Bibliography) error {
94130
return r.writeAll(all)
95131
}
96132

97-
func (r *CSVBibliographyRepository) FindAll() ([]*domain.Bibliography, error) {
133+
func (r *CSVBibliographyRepository) FindAll(limit, offset int) ([]*domain.Bibliography, error) {
98134
records, err := ReadCSV(r.FilePath)
99135
if err != nil {
100136
return nil, err
101137
}
102138

103-
var bibliographies []*domain.Bibliography
104139
// Skip header
105140
if len(records) > 0 {
106141
records = records[1:]
107142
}
108143

109-
for _, record := range records {
144+
iter := NewCSVRecordIterator(records, limit, offset)
145+
var bibliographies []*domain.Bibliography
146+
147+
for iter.Next() {
148+
record := iter.Record()
110149
if len(record) < 9 {
111150
continue
112151
}
@@ -131,39 +170,87 @@ func (r *CSVBibliographyRepository) FindAll() ([]*domain.Bibliography, error) {
131170

132171
bibliographies = append(bibliographies, bib)
133172
}
134-
return bibliographies, nil
173+
174+
return bibliographies, iter.Err()
135175
}
136176

137177
// FindByBibIndex implements domain.BibliographyRepository.FindByBibIndex
138-
// Performance Note: This method calls FindAll() which reads and parses the entire CSV file.
139-
// For large datasets, consider implementing caching or using a database for production use.
140178
func (r *CSVBibliographyRepository) FindByBibIndex(bibIndex string) (*domain.Bibliography, error) {
141-
all, err := r.FindAll()
179+
records, err := ReadCSV(r.FilePath)
142180
if err != nil {
143181
return nil, err
144182
}
145-
for _, b := range all {
146-
if b.BibIndex == bibIndex {
147-
return b, nil
183+
184+
// Skip header
185+
if len(records) > 0 {
186+
records = records[1:]
187+
}
188+
189+
iter := NewCSVRecordIterator(records, 0, 0)
190+
191+
for iter.Next() {
192+
record := iter.Record()
193+
if len(record) < 9 {
194+
continue
195+
}
196+
// Optimization: Check BibIndex (index 1) before full conversion
197+
if record[1] == bibIndex {
198+
bibRecord := &BibliographyRecord{
199+
ID: record[0],
200+
BibIndex: record[1],
201+
Code: record[2],
202+
Type: record[3],
203+
Title: record[4],
204+
Author: record[5],
205+
Publisher: record[6],
206+
ISBN: record[7],
207+
PublishedDate: record[8],
208+
}
209+
return recordToBibliography(bibRecord)
148210
}
149211
}
150-
return nil, nil
212+
213+
return nil, iter.Err()
151214
}
152215

153216
// FindByID implements domain.BibliographyRepository.FindByID
154-
// Performance Note: This method calls FindAll() which reads and parses the entire CSV file.
155-
// For large datasets, consider implementing caching or using a database for production use.
156217
func (r *CSVBibliographyRepository) FindByID(id domain.BibliographyID) (*domain.Bibliography, error) {
157-
all, err := r.FindAll()
218+
records, err := ReadCSV(r.FilePath)
158219
if err != nil {
159220
return nil, err
160221
}
161-
for _, b := range all {
162-
if b.ID == id {
163-
return b, nil
222+
223+
// Skip header
224+
if len(records) > 0 {
225+
records = records[1:]
226+
}
227+
228+
iter := NewCSVRecordIterator(records, 0, 0)
229+
idStr := id.String()
230+
231+
for iter.Next() {
232+
record := iter.Record()
233+
if len(record) < 9 {
234+
continue
235+
}
236+
// Optimization: Check ID (index 0) before full conversion
237+
if record[0] == idStr {
238+
bibRecord := &BibliographyRecord{
239+
ID: record[0],
240+
BibIndex: record[1],
241+
Code: record[2],
242+
Type: record[3],
243+
Title: record[4],
244+
Author: record[5],
245+
Publisher: record[6],
246+
ISBN: record[7],
247+
PublishedDate: record[8],
248+
}
249+
return recordToBibliography(bibRecord)
164250
}
165251
}
166-
return nil, nil
252+
253+
return nil, iter.Err()
167254
}
168255

169256
func (r *CSVBibliographyRepository) writeAll(bibliographies []*domain.Bibliography) error {

internal/infrastructure/bibliography_repository_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func TestCSVBibliographyRepository_SaveAndFind(t *testing.T) {
4242
t.Fatalf("Failed to save bibliography: %v", err)
4343
}
4444

45-
// Test FindAll
46-
all, err := repo.FindAll()
45+
// Test FindAll (get all records with limit=0, offset=0)
46+
all, err := repo.FindAll(0, 0)
4747
if err != nil {
4848
t.Fatalf("Failed to find all: %v", err)
4949
}

internal/infrastructure/classification_repository.go

Lines changed: 65 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,41 @@ func NewCSVClassificationRepository(filePath string) *CSVClassificationRepositor
5656
// without any locking mechanism. Acceptable for single-user CLI usage, but consider file locking
5757
// or using a database with proper transaction support for production use.
5858
func (r *CSVClassificationRepository) Save(c *domain.Classification) error {
59-
all, err := r.FindAll()
59+
records, err := ReadCSV(r.FilePath)
6060
if err != nil {
6161
return err
6262
}
6363

64+
// Skip header
65+
if len(records) > 0 {
66+
records = records[1:]
67+
}
68+
69+
iter := NewCSVRecordIterator(records, 0, 0)
70+
var all []*domain.Classification
71+
72+
for iter.Next() {
73+
record := iter.Record()
74+
if len(record) < 3 {
75+
continue
76+
}
77+
classRecord := &ClassificationRecord{
78+
ID: record[0],
79+
CodeNum: record[1],
80+
Name: record[2],
81+
}
82+
class, err := recordToClassification(classRecord)
83+
if err != nil {
84+
slog.Error("Failed to convert classification record", "err", err)
85+
continue
86+
}
87+
all = append(all, class)
88+
}
89+
90+
if iter.Err() != nil {
91+
return iter.Err()
92+
}
93+
6494
updated := false
6595
for i, existing := range all {
6696
if existing.ID == c.ID {
@@ -76,18 +106,22 @@ func (r *CSVClassificationRepository) Save(c *domain.Classification) error {
76106
return r.writeAll(all)
77107
}
78108

79-
func (r *CSVClassificationRepository) FindAll() ([]*domain.Classification, error) {
109+
func (r *CSVClassificationRepository) FindAll(limit, offset int) ([]*domain.Classification, error) {
80110
records, err := ReadCSV(r.FilePath)
81111
if err != nil {
82112
return nil, err
83113
}
84114

85-
var classifications []*domain.Classification
115+
// Skip header
86116
if len(records) > 0 {
87117
records = records[1:]
88118
}
89119

90-
for _, record := range records {
120+
iter := NewCSVRecordIterator(records, limit, offset)
121+
var classifications []*domain.Classification
122+
123+
for iter.Next() {
124+
record := iter.Record()
91125
if len(record) < 3 {
92126
continue
93127
}
@@ -106,20 +140,41 @@ func (r *CSVClassificationRepository) FindAll() ([]*domain.Classification, error
106140

107141
classifications = append(classifications, class)
108142
}
109-
return classifications, nil
143+
144+
return classifications, iter.Err()
110145
}
111146

112147
func (r *CSVClassificationRepository) FindByCodeNum(codeNum int) (*domain.Classification, error) {
113-
all, err := r.FindAll()
148+
records, err := ReadCSV(r.FilePath)
114149
if err != nil {
115150
return nil, err
116151
}
117-
for _, c := range all {
118-
if c.CodeNum == codeNum {
119-
return c, nil
152+
153+
// Skip header
154+
if len(records) > 0 {
155+
records = records[1:]
156+
}
157+
158+
iter := NewCSVRecordIterator(records, 0, 0)
159+
codeNumStr := strconv.Itoa(codeNum)
160+
161+
for iter.Next() {
162+
record := iter.Record()
163+
if len(record) < 3 {
164+
continue
165+
}
166+
// Optimization: Check CodeNum (index 1) before full conversion
167+
if record[1] == codeNumStr {
168+
classRecord := &ClassificationRecord{
169+
ID: record[0],
170+
CodeNum: record[1],
171+
Name: record[2],
172+
}
173+
return recordToClassification(classRecord)
120174
}
121175
}
122-
return nil, nil
176+
177+
return nil, iter.Err()
123178
}
124179

125180
func (r *CSVClassificationRepository) writeAll(classifications []*domain.Classification) error {

0 commit comments

Comments
 (0)