1// Package sqlite provides SQLite implementations of domain repositories.
2//
3// # Lazy Repository Infrastructure
4//
5// This file contains lazy-loading repository wrappers that defer database initialization
6// until first access. While currently unused (the application uses eager initialization
7// via OpenDatabase in RunParallelDBWebKit), this infrastructure is kept for potential
8// future optimization scenarios:
9//
10// - CLI commands that may not need database access
11// - Further startup latency reduction by deferring DB init past first paint
12// - Testing with mock DatabaseProvider implementations
13//
14// The lazy wrappers implement the same repository interfaces as their eager counterparts,
15// making them drop-in replacements when lazy initialization becomes beneficial.
16package sqlite
17
18import (
19 "context"
20 "database/sql"
21 "sync"
22 "time"
23
24 "github.com/bnema/dumber/internal/application/port"
25 "github.com/bnema/dumber/internal/domain/entity"
26 "github.com/bnema/dumber/internal/domain/repository"
27)
28
29// LazyHistoryRepository wraps a history repository with lazy database initialization.
30type LazyHistoryRepository struct {
31 provider port.DatabaseProvider
32 repo repository.HistoryRepository
33 once sync.Once
34 initErr error
35}
36
37// NewLazyHistoryRepository creates a lazy-loading history repository.
38func NewLazyHistoryRepository(provider port.DatabaseProvider) repository.HistoryRepository {
39 return &LazyHistoryRepository{provider: provider}
40}
41
42func (r *LazyHistoryRepository) init(ctx context.Context) error {
43 r.once.Do(func() {
44 db, err := r.provider.DB(ctx)
45 if err != nil {
46 r.initErr = err
47 return
48 }
49 r.repo = NewHistoryRepository(db)
50 })
51 return r.initErr
52}
53
54func (r *LazyHistoryRepository) Save(ctx context.Context, entry *entity.HistoryEntry) error {
55 if err := r.init(ctx); err != nil {
56 return err
57 }
58 return r.repo.Save(ctx, entry)
59}
60
61func (r *LazyHistoryRepository) FindByURL(ctx context.Context, url string) (*entity.HistoryEntry, error) {
62 if err := r.init(ctx); err != nil {
63 return nil, err
64 }
65 return r.repo.FindByURL(ctx, url)
66}
67
68func (r *LazyHistoryRepository) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) {
69 if err := r.init(ctx); err != nil {
70 return nil, err
71 }
72 return r.repo.Search(ctx, query, limit)
73}
74
75func (r *LazyHistoryRepository) GetRecent(ctx context.Context, limit, offset int) ([]*entity.HistoryEntry, error) {
76 if err := r.init(ctx); err != nil {
77 return nil, err
78 }
79 return r.repo.GetRecent(ctx, limit, offset)
80}
81
82func (r *LazyHistoryRepository) GetRecentSince(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
83 if err := r.init(ctx); err != nil {
84 return nil, err
85 }
86 return r.repo.GetRecentSince(ctx, days)
87}
88
89func (r *LazyHistoryRepository) GetMostVisited(ctx context.Context, days int) ([]*entity.HistoryEntry, error) {
90 if err := r.init(ctx); err != nil {
91 return nil, err
92 }
93 return r.repo.GetMostVisited(ctx, days)
94}
95
96func (r *LazyHistoryRepository) GetAllRecentHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
97 if err := r.init(ctx); err != nil {
98 return nil, err
99 }
100 return r.repo.GetAllRecentHistory(ctx)
101}
102
103func (r *LazyHistoryRepository) GetAllMostVisited(ctx context.Context) ([]*entity.HistoryEntry, error) {
104 if err := r.init(ctx); err != nil {
105 return nil, err
106 }
107 return r.repo.GetAllMostVisited(ctx)
108}
109
110func (r *LazyHistoryRepository) IncrementVisitCount(ctx context.Context, url string) error {
111 if err := r.init(ctx); err != nil {
112 return err
113 }
114 return r.repo.IncrementVisitCount(ctx, url)
115}
116
117func (r *LazyHistoryRepository) Delete(ctx context.Context, id int64) error {
118 if err := r.init(ctx); err != nil {
119 return err
120 }
121 return r.repo.Delete(ctx, id)
122}
123
124func (r *LazyHistoryRepository) DeleteOlderThan(ctx context.Context, before time.Time) error {
125 if err := r.init(ctx); err != nil {
126 return err
127 }
128 return r.repo.DeleteOlderThan(ctx, before)
129}
130
131func (r *LazyHistoryRepository) DeleteAll(ctx context.Context) error {
132 if err := r.init(ctx); err != nil {
133 return err
134 }
135 return r.repo.DeleteAll(ctx)
136}
137
138func (r *LazyHistoryRepository) DeleteByDomain(ctx context.Context, domain string) error {
139 if err := r.init(ctx); err != nil {
140 return err
141 }
142 return r.repo.DeleteByDomain(ctx, domain)
143}
144
145func (r *LazyHistoryRepository) GetStats(ctx context.Context) (*entity.HistoryStats, error) {
146 if err := r.init(ctx); err != nil {
147 return nil, err
148 }
149 return r.repo.GetStats(ctx)
150}
151
152func (r *LazyHistoryRepository) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) {
153 if err := r.init(ctx); err != nil {
154 return nil, err
155 }
156 return r.repo.GetDomainStats(ctx, limit)
157}
158
159func (r *LazyHistoryRepository) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) {
160 if err := r.init(ctx); err != nil {
161 return nil, err
162 }
163 return r.repo.GetHourlyDistribution(ctx)
164}
165
166func (r *LazyHistoryRepository) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) {
167 if err := r.init(ctx); err != nil {
168 return nil, err
169 }
170 return r.repo.GetDailyVisitCount(ctx, daysAgo)
171}
172
173// LazyFavoriteRepository wraps a favorite repository with lazy database initialization.
174type LazyFavoriteRepository struct {
175 provider port.DatabaseProvider
176 repo repository.FavoriteRepository
177 once sync.Once
178 initErr error
179}
180
181// NewLazyFavoriteRepository creates a lazy-loading favorite repository.
182func NewLazyFavoriteRepository(provider port.DatabaseProvider) repository.FavoriteRepository {
183 return &LazyFavoriteRepository{provider: provider}
184}
185
186func (r *LazyFavoriteRepository) init(ctx context.Context) error {
187 r.once.Do(func() {
188 db, err := r.provider.DB(ctx)
189 if err != nil {
190 r.initErr = err
191 return
192 }
193 r.repo = NewFavoriteRepository(db)
194 })
195 return r.initErr
196}
197
198func (r *LazyFavoriteRepository) Save(ctx context.Context, fav *entity.Favorite) error {
199 if err := r.init(ctx); err != nil {
200 return err
201 }
202 return r.repo.Save(ctx, fav)
203}
204
205func (r *LazyFavoriteRepository) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
206 if err := r.init(ctx); err != nil {
207 return nil, err
208 }
209 return r.repo.FindByID(ctx, id)
210}
211
212func (r *LazyFavoriteRepository) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
213 if err := r.init(ctx); err != nil {
214 return nil, err
215 }
216 return r.repo.FindByURL(ctx, url)
217}
218
219func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
220 if err := r.init(ctx); err != nil {
221 return nil, err
222 }
223 return r.repo.GetAll(ctx)
224}
225
226func (r *LazyFavoriteRepository) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
227 if err := r.init(ctx); err != nil {
228 return nil, err
229 }
230 return r.repo.GetByFolder(ctx, folderID)
231}
232
233func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
234 if err := r.init(ctx); err != nil {
235 return nil, err
236 }
237 return r.repo.GetByShortcut(ctx, key)
238}
239
240func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
241 if err := r.init(ctx); err != nil {
242 return err
243 }
244 return r.repo.UpdatePosition(ctx, id, position)
245}
246
247func (r *LazyFavoriteRepository) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
248 if err := r.init(ctx); err != nil {
249 return err
250 }
251 return r.repo.SetFolder(ctx, id, folderID)
252}
253
254func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
255 if err := r.init(ctx); err != nil {
256 return err
257 }
258 return r.repo.SetShortcut(ctx, id, key)
259}
260
261func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteID) error {
262 if err := r.init(ctx); err != nil {
263 return err
264 }
265 return r.repo.Delete(ctx, id)
266}
267
268// LazyFolderRepository wraps a folder repository with lazy database initialization.
269type LazyFolderRepository struct {
270 provider port.DatabaseProvider
271 repo repository.FolderRepository
272 once sync.Once
273 initErr error
274}
275
276// NewLazyFolderRepository creates a lazy-loading folder repository.
277func NewLazyFolderRepository(provider port.DatabaseProvider) repository.FolderRepository {
278 return &LazyFolderRepository{provider: provider}
279}
280
281func (r *LazyFolderRepository) init(ctx context.Context) error {
282 r.once.Do(func() {
283 db, err := r.provider.DB(ctx)
284 if err != nil {
285 r.initErr = err
286 return
287 }
288 r.repo = NewFolderRepository(db)
289 })
290 return r.initErr
291}
292
293func (r *LazyFolderRepository) Save(ctx context.Context, folder *entity.Folder) error {
294 if err := r.init(ctx); err != nil {
295 return err
296 }
297 return r.repo.Save(ctx, folder)
298}
299
300func (r *LazyFolderRepository) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
301 if err := r.init(ctx); err != nil {
302 return nil, err
303 }
304 return r.repo.FindByID(ctx, id)
305}
306
307func (r *LazyFolderRepository) GetAll(ctx context.Context) ([]*entity.Folder, error) {
308 if err := r.init(ctx); err != nil {
309 return nil, err
310 }
311 return r.repo.GetAll(ctx)
312}
313
314func (r *LazyFolderRepository) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
315 if err := r.init(ctx); err != nil {
316 return nil, err
317 }
318 return r.repo.GetChildren(ctx, parentID)
319}
320
321func (r *LazyFolderRepository) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
322 if err := r.init(ctx); err != nil {
323 return err
324 }
325 return r.repo.UpdatePosition(ctx, id, position)
326}
327
328func (r *LazyFolderRepository) Delete(ctx context.Context, id entity.FolderID) error {
329 if err := r.init(ctx); err != nil {
330 return err
331 }
332 return r.repo.Delete(ctx, id)
333}
334
335// LazyTagRepository wraps a tag repository with lazy database initialization.
336type LazyTagRepository struct {
337 provider port.DatabaseProvider
338 repo repository.TagRepository
339 once sync.Once
340 initErr error
341}
342
343// NewLazyTagRepository creates a lazy-loading tag repository.
344func NewLazyTagRepository(provider port.DatabaseProvider) repository.TagRepository {
345 return &LazyTagRepository{provider: provider}
346}
347
348func (r *LazyTagRepository) init(ctx context.Context) error {
349 r.once.Do(func() {
350 db, err := r.provider.DB(ctx)
351 if err != nil {
352 r.initErr = err
353 return
354 }
355 r.repo = NewTagRepository(db)
356 })
357 return r.initErr
358}
359
360func (r *LazyTagRepository) Save(ctx context.Context, tag *entity.Tag) error {
361 if err := r.init(ctx); err != nil {
362 return err
363 }
364 return r.repo.Save(ctx, tag)
365}
366
367func (r *LazyTagRepository) FindByID(ctx context.Context, id entity.TagID) (*entity.Tag, error) {
368 if err := r.init(ctx); err != nil {
369 return nil, err
370 }
371 return r.repo.FindByID(ctx, id)
372}
373
374func (r *LazyTagRepository) FindByName(ctx context.Context, name string) (*entity.Tag, error) {
375 if err := r.init(ctx); err != nil {
376 return nil, err
377 }
378 return r.repo.FindByName(ctx, name)
379}
380
381func (r *LazyTagRepository) GetAll(ctx context.Context) ([]*entity.Tag, error) {
382 if err := r.init(ctx); err != nil {
383 return nil, err
384 }
385 return r.repo.GetAll(ctx)
386}
387
388func (r *LazyTagRepository) AssignToFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
389 if err := r.init(ctx); err != nil {
390 return err
391 }
392 return r.repo.AssignToFavorite(ctx, tagID, favID)
393}
394
395func (r *LazyTagRepository) RemoveFromFavorite(ctx context.Context, tagID entity.TagID, favID entity.FavoriteID) error {
396 if err := r.init(ctx); err != nil {
397 return err
398 }
399 return r.repo.RemoveFromFavorite(ctx, tagID, favID)
400}
401
402func (r *LazyTagRepository) GetForFavorite(ctx context.Context, favID entity.FavoriteID) ([]*entity.Tag, error) {
403 if err := r.init(ctx); err != nil {
404 return nil, err
405 }
406 return r.repo.GetForFavorite(ctx, favID)
407}
408
409func (r *LazyTagRepository) Delete(ctx context.Context, id entity.TagID) error {
410 if err := r.init(ctx); err != nil {
411 return err
412 }
413 return r.repo.Delete(ctx, id)
414}
415
416// LazyZoomRepository wraps a zoom repository with lazy database initialization.
417type LazyZoomRepository struct {
418 provider port.DatabaseProvider
419 repo repository.ZoomRepository
420 once sync.Once
421 initErr error
422}
423
424// NewLazyZoomRepository creates a lazy-loading zoom repository.
425func NewLazyZoomRepository(provider port.DatabaseProvider) repository.ZoomRepository {
426 return &LazyZoomRepository{provider: provider}
427}
428
429func (r *LazyZoomRepository) init(ctx context.Context) error {
430 r.once.Do(func() {
431 db, err := r.provider.DB(ctx)
432 if err != nil {
433 r.initErr = err
434 return
435 }
436 r.repo = NewZoomRepository(db)
437 })
438 return r.initErr
439}
440
441func (r *LazyZoomRepository) Get(ctx context.Context, domain string) (*entity.ZoomLevel, error) {
442 if err := r.init(ctx); err != nil {
443 return nil, err
444 }
445 return r.repo.Get(ctx, domain)
446}
447
448func (r *LazyZoomRepository) Set(ctx context.Context, zoom *entity.ZoomLevel) error {
449 if err := r.init(ctx); err != nil {
450 return err
451 }
452 return r.repo.Set(ctx, zoom)
453}
454
455func (r *LazyZoomRepository) Delete(ctx context.Context, domain string) error {
456 if err := r.init(ctx); err != nil {
457 return err
458 }
459 return r.repo.Delete(ctx, domain)
460}
461
462func (r *LazyZoomRepository) GetAll(ctx context.Context) ([]*entity.ZoomLevel, error) {
463 if err := r.init(ctx); err != nil {
464 return nil, err
465 }
466 return r.repo.GetAll(ctx)
467}
468
469// LazySessionRepository wraps a session repository with lazy database initialization.
470type LazySessionRepository struct {
471 provider port.DatabaseProvider
472 repo repository.SessionRepository
473 once sync.Once
474 initErr error
475}
476
477// NewLazySessionRepository creates a lazy-loading session repository.
478func NewLazySessionRepository(provider port.DatabaseProvider) repository.SessionRepository {
479 return &LazySessionRepository{provider: provider}
480}
481
482func (r *LazySessionRepository) init(ctx context.Context) error {
483 r.once.Do(func() {
484 db, err := r.provider.DB(ctx)
485 if err != nil {
486 r.initErr = err
487 return
488 }
489 r.repo = NewSessionRepository(db)
490 })
491 return r.initErr
492}
493
494func (r *LazySessionRepository) Save(ctx context.Context, session *entity.Session) error {
495 if err := r.init(ctx); err != nil {
496 return err
497 }
498 return r.repo.Save(ctx, session)
499}
500
501func (r *LazySessionRepository) FindByID(ctx context.Context, id entity.SessionID) (*entity.Session, error) {
502 if err := r.init(ctx); err != nil {
503 return nil, err
504 }
505 return r.repo.FindByID(ctx, id)
506}
507
508func (r *LazySessionRepository) GetActive(ctx context.Context) (*entity.Session, error) {
509 if err := r.init(ctx); err != nil {
510 return nil, err
511 }
512 return r.repo.GetActive(ctx)
513}
514
515func (r *LazySessionRepository) GetRecent(ctx context.Context, limit int) ([]*entity.Session, error) {
516 if err := r.init(ctx); err != nil {
517 return nil, err
518 }
519 return r.repo.GetRecent(ctx, limit)
520}
521
522func (r *LazySessionRepository) MarkEnded(ctx context.Context, id entity.SessionID, endedAt time.Time) error {
523 if err := r.init(ctx); err != nil {
524 return err
525 }
526 return r.repo.MarkEnded(ctx, id, endedAt)
527}
528
529func (r *LazySessionRepository) Delete(ctx context.Context, id entity.SessionID) error {
530 if err := r.init(ctx); err != nil {
531 return err
532 }
533 return r.repo.Delete(ctx, id)
534}
535
536func (r *LazySessionRepository) DeleteOldestExited(ctx context.Context, keepCount int) (int64, error) {
537 if err := r.init(ctx); err != nil {
538 return 0, err
539 }
540 return r.repo.DeleteOldestExited(ctx, keepCount)
541}
542
543func (r *LazySessionRepository) DeleteExitedBefore(ctx context.Context, cutoff time.Time) (int64, error) {
544 if err := r.init(ctx); err != nil {
545 return 0, err
546 }
547 return r.repo.DeleteExitedBefore(ctx, cutoff)
548}
549
550// LazySessionStateRepository wraps a session state repository with lazy database initialization.
551type LazySessionStateRepository struct {
552 provider port.DatabaseProvider
553 repo repository.SessionStateRepository
554 once sync.Once
555 initErr error
556}
557
558// NewLazySessionStateRepository creates a lazy-loading session state repository.
559func NewLazySessionStateRepository(provider port.DatabaseProvider) repository.SessionStateRepository {
560 return &LazySessionStateRepository{provider: provider}
561}
562
563func (r *LazySessionStateRepository) init(ctx context.Context) error {
564 r.once.Do(func() {
565 db, err := r.provider.DB(ctx)
566 if err != nil {
567 r.initErr = err
568 return
569 }
570 r.repo = NewSessionStateRepository(db)
571 })
572 return r.initErr
573}
574
575func (r *LazySessionStateRepository) SaveSnapshot(ctx context.Context, state *entity.SessionState) error {
576 if err := r.init(ctx); err != nil {
577 return err
578 }
579 return r.repo.SaveSnapshot(ctx, state)
580}
581
582func (r *LazySessionStateRepository) GetSnapshot(ctx context.Context, sessionID entity.SessionID) (*entity.SessionState, error) {
583 if err := r.init(ctx); err != nil {
584 return nil, err
585 }
586 return r.repo.GetSnapshot(ctx, sessionID)
587}
588
589func (r *LazySessionStateRepository) DeleteSnapshot(ctx context.Context, sessionID entity.SessionID) error {
590 if err := r.init(ctx); err != nil {
591 return err
592 }
593 return r.repo.DeleteSnapshot(ctx, sessionID)
594}
595
596func (r *LazySessionStateRepository) GetAllSnapshots(ctx context.Context) ([]*entity.SessionState, error) {
597 if err := r.init(ctx); err != nil {
598 return nil, err
599 }
600 return r.repo.GetAllSnapshots(ctx)
601}
602
603func (r *LazySessionStateRepository) GetTotalSnapshotsSize(ctx context.Context) (int64, error) {
604 if err := r.init(ctx); err != nil {
605 return 0, err
606 }
607 return r.repo.GetTotalSnapshotsSize(ctx)
608}
609
610// LazyContentWhitelistRepository wraps a content whitelist repository with lazy database initialization.
611type LazyContentWhitelistRepository struct {
612 provider port.DatabaseProvider
613 repo repository.ContentWhitelistRepository
614 once sync.Once
615 initErr error
616}
617
618// NewLazyContentWhitelistRepository creates a lazy-loading content whitelist repository.
619func NewLazyContentWhitelistRepository(provider port.DatabaseProvider) repository.ContentWhitelistRepository {
620 return &LazyContentWhitelistRepository{provider: provider}
621}
622
623func (r *LazyContentWhitelistRepository) init(ctx context.Context) error {
624 r.once.Do(func() {
625 db, err := r.provider.DB(ctx)
626 if err != nil {
627 r.initErr = err
628 return
629 }
630 r.repo = NewContentWhitelistRepository(db)
631 })
632 return r.initErr
633}
634
635func (r *LazyContentWhitelistRepository) Add(ctx context.Context, domain string) error {
636 if err := r.init(ctx); err != nil {
637 return err
638 }
639 return r.repo.Add(ctx, domain)
640}
641
642func (r *LazyContentWhitelistRepository) Remove(ctx context.Context, domain string) error {
643 if err := r.init(ctx); err != nil {
644 return err
645 }
646 return r.repo.Remove(ctx, domain)
647}
648
649func (r *LazyContentWhitelistRepository) Contains(ctx context.Context, domain string) (bool, error) {
650 if err := r.init(ctx); err != nil {
651 return false, err
652 }
653 return r.repo.Contains(ctx, domain)
654}
655
656func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string, error) {
657 if err := r.init(ctx); err != nil {
658 return nil, err
659 }
660 return r.repo.GetAll(ctx)
661}
662
663// LazyRepositories holds all lazy-loaded repositories.
664type LazyRepositories struct {
665 History repository.HistoryRepository
666 Favorite repository.FavoriteRepository
667 Folder repository.FolderRepository
668 Tag repository.TagRepository
669 Zoom repository.ZoomRepository
670 Session repository.SessionRepository
671 SessionState repository.SessionStateRepository
672 Filter repository.ContentWhitelistRepository
673}
674
675// NewLazyRepositories creates all lazy repositories from a database provider.
676func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
677 return &LazyRepositories{
678 History: NewLazyHistoryRepository(provider),
679 Favorite: NewLazyFavoriteRepository(provider),
680 Folder: NewLazyFolderRepository(provider),
681 Tag: NewLazyTagRepository(provider),
682 Zoom: NewLazyZoomRepository(provider),
683 Session: NewLazySessionRepository(provider),
684 SessionState: NewLazySessionStateRepository(provider),
685 Filter: NewLazyContentWhitelistRepository(provider),
686 }
687}
688
689// GetDB returns the underlying *sql.DB for operations that need it directly.
690// Returns nil if the database hasn't been initialized yet.
691func GetDB(provider port.DatabaseProvider, ctx context.Context) *sql.DB {
692 db, err := provider.DB(ctx)
693 if err != nil {
694 return nil
695 }
696 return db
697}