detached ecab2f7

refactor(favorites): migrate folders to tags

commit
ecab2f7f6c85d702ff41106bc7366dfa8695d9c2
author
brice · · 2026-06-28 07:17 UTC
parent
2ae1fc7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
12 files · +480 -577
M internal/infrastructure/persistence/sqlite/favorite_repo.go +62 -189
@@ -14,78 +14,38 @@ import (
1414 // ==================== Favorite Repository ====================
1515
1616 type favoriteRepo struct {
17- db *sql.DB
1817 queries *sqlc.Queries
1918 }
2019
2120 // NewFavoriteRepository creates a new SQLite-backed favorite repository.
2221 func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository {
23- return &favoriteRepo{db: db, queries: sqlc.New(db)}
22+ return &favoriteRepo{queries: sqlc.New(db)}
2423 }
2524
2625 func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error {
2726 log := logging.FromContext(ctx)
2827 log.Debug().Str("url", fav.URL).Int64("id", int64(fav.ID)).Msg("saving favorite")
2928
30- var folderID sql.NullInt64
31- if fav.FolderID != nil {
32- folderID = sql.NullInt64{Int64: int64(*fav.FolderID), Valid: true}
33- }
34-
3529 if fav.ID > 0 {
36- return r.updateExisting(ctx, fav, folderID)
30+ return r.queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
31+ Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
32+ FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
33+ ID: int64(fav.ID),
34+ })
3735 }
3836
3937 row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{
4038 Url: fav.URL,
4139 Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
4240 FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
43- FolderID: folderID,
4441 })
4542 if err != nil {
4643 return err
4744 }
48- fav.ID = entity.FavoriteID(row.ID)
45+ favoriteFromRow(row, fav)
4946 return nil
5047 }
5148
52-func (r *favoriteRepo) updateExisting(ctx context.Context, fav *entity.Favorite, folderID sql.NullInt64) error {
53- log := logging.FromContext(ctx)
54- tx, err := r.db.BeginTx(ctx, nil)
55- if err != nil {
56- return err
57- }
58- defer func() {
59- if rollbackErr := tx.Rollback(); rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
60- log.Debug().Err(rollbackErr).Msg("favorite update rollback reported non-terminal error")
61- }
62- }()
63-
64- shortcutKey := sql.NullInt64{}
65- if fav.ShortcutKey != nil {
66- shortcutKey = sql.NullInt64{Int64: int64(*fav.ShortcutKey), Valid: true}
67- }
68-
69- queries := r.queries.WithTx(tx)
70- if err := queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
71- Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
72- FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
73- ID: int64(fav.ID),
74- }); err != nil {
75- return err
76- }
77- if err := queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{FolderID: folderID, ID: int64(fav.ID)}); err != nil {
78- return err
79- }
80- if err := queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{
81- ShortcutKey: shortcutKey,
82- ID: int64(fav.ID),
83- }); err != nil {
84- return err
85- }
86- return tx.Commit()
87-}
88-
8949 func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
9050 row, err := r.queries.GetFavoriteByID(ctx, int64(id))
9151 if err != nil {
@@ -94,7 +54,8 @@ func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*ent
9454 }
9555 return nil, err
9656 }
97- return favoriteFromRow(row), nil
57+ fav := favoriteFromRow(row, nil)
58+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
9859 }
9960
10061 func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
@@ -105,7 +66,8 @@ func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favor
10566 }
10667 return nil, err
10768 }
108- return favoriteFromRow(row), nil
69+ fav := favoriteFromRow(row, nil)
70+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
10971 }
11072
11173 func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
@@ -113,22 +75,17 @@ func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
11375 if err != nil {
11476 return nil, err
11577 }
116- return favoritesFromRows(rows), nil
78+ favorites := favoritesFromRows(rows)
79+ return favorites, r.hydrateTags(ctx, favorites)
11780 }
11881
119-func (r *favoriteRepo) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
120- var rows []sqlc.Favorite
121- var err error
122-
123- if folderID == nil {
124- rows, err = r.queries.GetFavoritesWithoutFolder(ctx)
125- } else {
126- rows, err = r.queries.GetFavoritesByFolder(ctx, sql.NullInt64{Int64: int64(*folderID), Valid: true})
127- }
82+func (r *favoriteRepo) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
83+ rows, err := r.queries.GetFavoritesByTag(ctx, int64(tagID))
12884 if err != nil {
12985 return nil, err
13086 }
131- return favoritesFromRows(rows), nil
87+ favorites := favoritesFromRows(rows)
88+ return favorites, r.hydrateTags(ctx, favorites)
13289 }
13390
13491 func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
@@ -139,7 +96,8 @@ func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favo
13996 }
14097 return nil, err
14198 }
142- return favoriteFromRow(row), nil
99+ fav := favoriteFromRow(row, nil)
100+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
143101 }
144102
145103 func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
@@ -149,17 +107,6 @@ func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID,
149107 })
150108 }
151109
152-func (r *favoriteRepo) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
153- var fid sql.NullInt64
154- if folderID != nil {
155- fid = sql.NullInt64{Int64: int64(*folderID), Valid: true}
156- }
157- return r.queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{
158- FolderID: fid,
159- ID: int64(id),
160- })
161-}
162-
163110 func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
164111 var k sql.NullInt64
165112 if key != nil {
@@ -175,141 +122,67 @@ func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error {
175122 return r.queries.DeleteFavorite(ctx, int64(id))
176123 }
177124
178-func favoriteFromRow(row sqlc.Favorite) *entity.Favorite {
179- fav := &entity.Favorite{
180- ID: entity.FavoriteID(row.ID),
181- URL: row.Url,
182- Title: row.Title.String,
183- FaviconURL: row.FaviconUrl.String,
184- Position: int(row.Position),
185- CreatedAt: row.CreatedAt.Time,
186- UpdatedAt: row.UpdatedAt.Time,
187- }
188- if row.FolderID.Valid {
189- fid := entity.FolderID(row.FolderID.Int64)
190- fav.FolderID = &fid
191- }
192- if row.ShortcutKey.Valid {
193- key := int(row.ShortcutKey.Int64)
194- fav.ShortcutKey = &key
195- }
196- return fav
197-}
198-
199-func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite {
200- favorites := make([]*entity.Favorite, len(rows))
201- for i := range rows {
202- favorites[i] = favoriteFromRow(rows[i])
125+func (r *favoriteRepo) hydrateTags(ctx context.Context, favorites []*entity.Favorite) error {
126+ if len(favorites) == 0 {
127+ return nil
203128 }
204- return favorites
205-}
206-
207-// ==================== Folder Repository ====================
208-
209-type folderRepo struct {
210- queries *sqlc.Queries
211-}
212129
213-// NewFolderRepository creates a new SQLite-backed folder repository.
214-func NewFolderRepository(db *sql.DB) repository.FolderRepository {
215- return &folderRepo{queries: sqlc.New(db)}
216-}
217-
218-func (r *folderRepo) Save(ctx context.Context, folder *entity.Folder) error {
219- log := logging.FromContext(ctx)
220- log.Debug().Str("name", folder.Name).Int64("id", int64(folder.ID)).Msg("saving folder")
221-
222- var parentID sql.NullInt64
223- if folder.ParentID != nil {
224- parentID = sql.NullInt64{Int64: int64(*folder.ParentID), Valid: true}
130+ ids := make([]int64, 0, len(favorites))
131+ byID := make(map[entity.FavoriteID]*entity.Favorite, len(favorites))
132+ for _, fav := range favorites {
133+ if fav == nil {
134+ continue
135+ }
136+ fav.Tags = []entity.Tag{}
137+ ids = append(ids, int64(fav.ID))
138+ byID[fav.ID] = fav
225139 }
226-
227- if folder.ID > 0 {
228- return r.queries.UpdateFolder(ctx, sqlc.UpdateFolderParams{
229- Name: folder.Name,
230- Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
231- ID: int64(folder.ID),
232- })
140+ if len(ids) == 0 {
141+ return nil
233142 }
234143
235- row, err := r.queries.CreateFolder(ctx, sqlc.CreateFolderParams{
236- Name: folder.Name,
237- Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
238- ParentID: parentID,
239- })
144+ rows, err := r.queries.GetTagsForFavorites(ctx, ids)
240145 if err != nil {
241146 return err
242147 }
243- folder.ID = entity.FolderID(row.ID)
244- return nil
245-}
246-
247-func (r *folderRepo) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
248- row, err := r.queries.GetFolderByID(ctx, int64(id))
249- if err != nil {
250- if errors.Is(err, sql.ErrNoRows) {
251- return nil, nil
148+ for _, row := range rows {
149+ fav := byID[entity.FavoriteID(row.FavoriteID)]
150+ if fav == nil {
151+ continue
252152 }
253- return nil, err
153+ fav.Tags = append(fav.Tags, *tagFromRow(row.FavoriteTag))
254154 }
255- return folderFromRow(row), nil
155+ return nil
256156 }
257157
258-func (r *folderRepo) GetAll(ctx context.Context) ([]*entity.Folder, error) {
259- rows, err := r.queries.GetAllFolders(ctx)
260- if err != nil {
261- return nil, err
158+func favoriteFromRow(row sqlc.Favorite, target *entity.Favorite) *entity.Favorite {
159+ fav := target
160+ if fav == nil {
161+ fav = &entity.Favorite{}
262162 }
263- return foldersFromRows(rows), nil
264-}
265-
266-func (r *folderRepo) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
267- var rows []sqlc.FavoriteFolder
268- var err error
269-
270- if parentID == nil {
271- rows, err = r.queries.GetRootFolders(ctx)
163+ fav.ID = entity.FavoriteID(row.ID)
164+ fav.URL = row.Url
165+ fav.Title = row.Title.String
166+ fav.FaviconURL = row.FaviconUrl.String
167+ fav.Position = int(row.Position)
168+ fav.CreatedAt = row.CreatedAt.Time
169+ fav.UpdatedAt = row.UpdatedAt.Time
170+ fav.Tags = nil
171+ if row.ShortcutKey.Valid {
172+ key := int(row.ShortcutKey.Int64)
173+ fav.ShortcutKey = &key
272174 } else {
273- rows, err = r.queries.GetChildFolders(ctx, sql.NullInt64{Int64: int64(*parentID), Valid: true})
274- }
275- if err != nil {
276- return nil, err
175+ fav.ShortcutKey = nil
277176 }
278- return foldersFromRows(rows), nil
279-}
280-
281-func (r *folderRepo) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
282- return r.queries.UpdateFolderPosition(ctx, sqlc.UpdateFolderPositionParams{
283- Position: int64(position),
284- ID: int64(id),
285- })
286-}
287-
288-func (r *folderRepo) Delete(ctx context.Context, id entity.FolderID) error {
289- return r.queries.DeleteFolder(ctx, int64(id))
290-}
291-
292-func folderFromRow(row sqlc.FavoriteFolder) *entity.Folder {
293- folder := &entity.Folder{
294- ID: entity.FolderID(row.ID),
295- Name: row.Name,
296- Icon: row.Icon.String,
297- Position: int(row.Position),
298- CreatedAt: row.CreatedAt.Time,
299- }
300- if row.ParentID.Valid {
301- pid := entity.FolderID(row.ParentID.Int64)
302- folder.ParentID = &pid
303- }
304- return folder
177+ return fav
305178 }
306179
307-func foldersFromRows(rows []sqlc.FavoriteFolder) []*entity.Folder {
308- folders := make([]*entity.Folder, len(rows))
309- for i, row := range rows {
310- folders[i] = folderFromRow(row)
180+func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite {
181+ favorites := make([]*entity.Favorite, len(rows))
182+ for i := range rows {
183+ favorites[i] = favoriteFromRow(rows[i], nil)
311184 }
312- return folders
185+ return favorites
313186 }
314187
315188 // ==================== Tag Repository ====================
@@ -14,78 +14,38 @@ import (
14 // ==================== Favorite Repository ====================14 // ==================== Favorite Repository ====================
15 15
16 type favoriteRepo struct {16 type favoriteRepo struct {
17- db *sql.DB
18 queries *sqlc.Queries17 queries *sqlc.Queries
19 }18 }
20 19
21 // NewFavoriteRepository creates a new SQLite-backed favorite repository.20 // NewFavoriteRepository creates a new SQLite-backed favorite repository.
22 func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository {21 func NewFavoriteRepository(db *sql.DB) repository.FavoriteRepository {
23- return &favoriteRepo{db: db, queries: sqlc.New(db)}22+ return &favoriteRepo{queries: sqlc.New(db)}
24 }23 }
25 24
26 func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error {25 func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error {
27 log := logging.FromContext(ctx)26 log := logging.FromContext(ctx)
28 log.Debug().Str("url", fav.URL).Int64("id", int64(fav.ID)).Msg("saving favorite")27 log.Debug().Str("url", fav.URL).Int64("id", int64(fav.ID)).Msg("saving favorite")
29 28
30- var folderID sql.NullInt64
31- if fav.FolderID != nil {
32- folderID = sql.NullInt64{Int64: int64(*fav.FolderID), Valid: true}
33- }
34-
35 if fav.ID > 0 {29 if fav.ID > 0 {
36- return r.updateExisting(ctx, fav, folderID)30+ return r.queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
31+ Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
32+ FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
33+ ID: int64(fav.ID),
34+ })
37 }35 }
38 36
39 row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{37 row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{
40 Url: fav.URL,38 Url: fav.URL,
41 Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},39 Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
42 FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},40 FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
43- FolderID: folderID,
44 })41 })
45 if err != nil {42 if err != nil {
46 return err43 return err
47 }44 }
48- fav.ID = entity.FavoriteID(row.ID)45+ favoriteFromRow(row, fav)
49 return nil46 return nil
50 }47 }
51 48
52-func (r *favoriteRepo) updateExisting(ctx context.Context, fav *entity.Favorite, folderID sql.NullInt64) error {
53- log := logging.FromContext(ctx)
54- tx, err := r.db.BeginTx(ctx, nil)
55- if err != nil {
56- return err
57- }
58- defer func() {
59- if rollbackErr := tx.Rollback(); rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
60- log.Debug().Err(rollbackErr).Msg("favorite update rollback reported non-terminal error")
61- }
62- }()
63-
64- shortcutKey := sql.NullInt64{}
65- if fav.ShortcutKey != nil {
66- shortcutKey = sql.NullInt64{Int64: int64(*fav.ShortcutKey), Valid: true}
67- }
68-
69- queries := r.queries.WithTx(tx)
70- if err := queries.UpdateFavorite(ctx, sqlc.UpdateFavoriteParams{
71- Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""},
72- FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""},
73- ID: int64(fav.ID),
74- }); err != nil {
75- return err
76- }
77- if err := queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{FolderID: folderID, ID: int64(fav.ID)}); err != nil {
78- return err
79- }
80- if err := queries.SetFavoriteShortcut(ctx, sqlc.SetFavoriteShortcutParams{
81- ShortcutKey: shortcutKey,
82- ID: int64(fav.ID),
83- }); err != nil {
84- return err
85- }
86- return tx.Commit()
87-}
88-
89 func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {49 func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) {
90 row, err := r.queries.GetFavoriteByID(ctx, int64(id))50 row, err := r.queries.GetFavoriteByID(ctx, int64(id))
91 if err != nil {51 if err != nil {
@@ -94,7 +54,8 @@ func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*ent
94 }54 }
95 return nil, err55 return nil, err
96 }56 }
97- return favoriteFromRow(row), nil57+ fav := favoriteFromRow(row, nil)
58+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
98 }59 }
99 60
100 func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {61 func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favorite, error) {
@@ -105,7 +66,8 @@ func (r *favoriteRepo) FindByURL(ctx context.Context, url string) (*entity.Favor
105 }66 }
106 return nil, err67 return nil, err
107 }68 }
108- return favoriteFromRow(row), nil69+ fav := favoriteFromRow(row, nil)
70+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
109 }71 }
110 72
111 func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {73 func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
@@ -113,22 +75,17 @@ func (r *favoriteRepo) GetAll(ctx context.Context) ([]*entity.Favorite, error) {
113 if err != nil {75 if err != nil {
114 return nil, err76 return nil, err
115 }77 }
116- return favoritesFromRows(rows), nil78+ favorites := favoritesFromRows(rows)
79+ return favorites, r.hydrateTags(ctx, favorites)
117 }80 }
118 81
119-func (r *favoriteRepo) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {82+func (r *favoriteRepo) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
120- var rows []sqlc.Favorite83+ rows, err := r.queries.GetFavoritesByTag(ctx, int64(tagID))
121- var err error
122-
123- if folderID == nil {
124- rows, err = r.queries.GetFavoritesWithoutFolder(ctx)
125- } else {
126- rows, err = r.queries.GetFavoritesByFolder(ctx, sql.NullInt64{Int64: int64(*folderID), Valid: true})
127- }
128 if err != nil {84 if err != nil {
129 return nil, err85 return nil, err
130 }86 }
131- return favoritesFromRows(rows), nil87+ favorites := favoritesFromRows(rows)
88+ return favorites, r.hydrateTags(ctx, favorites)
132 }89 }
133 90
134 func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {91 func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
@@ -139,7 +96,8 @@ func (r *favoriteRepo) GetByShortcut(ctx context.Context, key int) (*entity.Favo
139 }96 }
140 return nil, err97 return nil, err
141 }98 }
142- return favoriteFromRow(row), nil99+ fav := favoriteFromRow(row, nil)
100+ return fav, r.hydrateTags(ctx, []*entity.Favorite{fav})
143 }101 }
144 102
145 func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {103 func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID, position int) error {
@@ -149,17 +107,6 @@ func (r *favoriteRepo) UpdatePosition(ctx context.Context, id entity.FavoriteID,
149 })107 })
150 }108 }
151 109
152-func (r *favoriteRepo) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
153- var fid sql.NullInt64
154- if folderID != nil {
155- fid = sql.NullInt64{Int64: int64(*folderID), Valid: true}
156- }
157- return r.queries.SetFavoriteFolder(ctx, sqlc.SetFavoriteFolderParams{
158- FolderID: fid,
159- ID: int64(id),
160- })
161-}
162-
163 func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {110 func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
164 var k sql.NullInt64111 var k sql.NullInt64
165 if key != nil {112 if key != nil {
@@ -175,141 +122,67 @@ func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error {
175 return r.queries.DeleteFavorite(ctx, int64(id))122 return r.queries.DeleteFavorite(ctx, int64(id))
176 }123 }
177 124
178-func favoriteFromRow(row sqlc.Favorite) *entity.Favorite {125+func (r *favoriteRepo) hydrateTags(ctx context.Context, favorites []*entity.Favorite) error {
179- fav := &entity.Favorite{126+ if len(favorites) == 0 {
180- ID: entity.FavoriteID(row.ID),127+ return nil
181- URL: row.Url,
182- Title: row.Title.String,
183- FaviconURL: row.FaviconUrl.String,
184- Position: int(row.Position),
185- CreatedAt: row.CreatedAt.Time,
186- UpdatedAt: row.UpdatedAt.Time,
187- }
188- if row.FolderID.Valid {
189- fid := entity.FolderID(row.FolderID.Int64)
190- fav.FolderID = &fid
191- }
192- if row.ShortcutKey.Valid {
193- key := int(row.ShortcutKey.Int64)
194- fav.ShortcutKey = &key
195- }
196- return fav
197-}
198-
199-func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite {
200- favorites := make([]*entity.Favorite, len(rows))
201- for i := range rows {
202- favorites[i] = favoriteFromRow(rows[i])
203 }128 }
204- return favorites
205-}
206-
207-// ==================== Folder Repository ====================
208-
209-type folderRepo struct {
210- queries *sqlc.Queries
211-}
212 129
213-// NewFolderRepository creates a new SQLite-backed folder repository.130+ ids := make([]int64, 0, len(favorites))
214-func NewFolderRepository(db *sql.DB) repository.FolderRepository {131+ byID := make(map[entity.FavoriteID]*entity.Favorite, len(favorites))
215- return &folderRepo{queries: sqlc.New(db)}132+ for _, fav := range favorites {
216-}133+ if fav == nil {
217-134+ continue
218-func (r *folderRepo) Save(ctx context.Context, folder *entity.Folder) error {135+ }
219- log := logging.FromContext(ctx)136+ fav.Tags = []entity.Tag{}
220- log.Debug().Str("name", folder.Name).Int64("id", int64(folder.ID)).Msg("saving folder")137+ ids = append(ids, int64(fav.ID))
221-138+ byID[fav.ID] = fav
222- var parentID sql.NullInt64
223- if folder.ParentID != nil {
224- parentID = sql.NullInt64{Int64: int64(*folder.ParentID), Valid: true}
225 }139 }
226-140+ if len(ids) == 0 {
227- if folder.ID > 0 {141+ return nil
228- return r.queries.UpdateFolder(ctx, sqlc.UpdateFolderParams{
229- Name: folder.Name,
230- Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
231- ID: int64(folder.ID),
232- })
233 }142 }
234 143
235- row, err := r.queries.CreateFolder(ctx, sqlc.CreateFolderParams{144+ rows, err := r.queries.GetTagsForFavorites(ctx, ids)
236- Name: folder.Name,
237- Icon: sql.NullString{String: folder.Icon, Valid: folder.Icon != ""},
238- ParentID: parentID,
239- })
240 if err != nil {145 if err != nil {
241 return err146 return err
242 }147 }
243- folder.ID = entity.FolderID(row.ID)148+ for _, row := range rows {
244- return nil149+ fav := byID[entity.FavoriteID(row.FavoriteID)]
245-}150+ if fav == nil {
246-151+ continue
247-func (r *folderRepo) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
248- row, err := r.queries.GetFolderByID(ctx, int64(id))
249- if err != nil {
250- if errors.Is(err, sql.ErrNoRows) {
251- return nil, nil
252 }152 }
253- return nil, err153+ fav.Tags = append(fav.Tags, *tagFromRow(row.FavoriteTag))
254 }154 }
255- return folderFromRow(row), nil155+ return nil
256 }156 }
257 157
258-func (r *folderRepo) GetAll(ctx context.Context) ([]*entity.Folder, error) {158+func favoriteFromRow(row sqlc.Favorite, target *entity.Favorite) *entity.Favorite {
259- rows, err := r.queries.GetAllFolders(ctx)159+ fav := target
260- if err != nil {160+ if fav == nil {
261- return nil, err161+ fav = &entity.Favorite{}
262 }162 }
263- return foldersFromRows(rows), nil163+ fav.ID = entity.FavoriteID(row.ID)
264-}164+ fav.URL = row.Url
265-165+ fav.Title = row.Title.String
266-func (r *folderRepo) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {166+ fav.FaviconURL = row.FaviconUrl.String
267- var rows []sqlc.FavoriteFolder167+ fav.Position = int(row.Position)
268- var err error168+ fav.CreatedAt = row.CreatedAt.Time
269-169+ fav.UpdatedAt = row.UpdatedAt.Time
270- if parentID == nil {170+ fav.Tags = nil
271- rows, err = r.queries.GetRootFolders(ctx)171+ if row.ShortcutKey.Valid {
172+ key := int(row.ShortcutKey.Int64)
173+ fav.ShortcutKey = &key
272 } else {174 } else {
273- rows, err = r.queries.GetChildFolders(ctx, sql.NullInt64{Int64: int64(*parentID), Valid: true})175+ fav.ShortcutKey = nil
274- }
275- if err != nil {
276- return nil, err
277 }176 }
278- return foldersFromRows(rows), nil177+ return fav
279-}
280-
281-func (r *folderRepo) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
282- return r.queries.UpdateFolderPosition(ctx, sqlc.UpdateFolderPositionParams{
283- Position: int64(position),
284- ID: int64(id),
285- })
286-}
287-
288-func (r *folderRepo) Delete(ctx context.Context, id entity.FolderID) error {
289- return r.queries.DeleteFolder(ctx, int64(id))
290-}
291-
292-func folderFromRow(row sqlc.FavoriteFolder) *entity.Folder {
293- folder := &entity.Folder{
294- ID: entity.FolderID(row.ID),
295- Name: row.Name,
296- Icon: row.Icon.String,
297- Position: int(row.Position),
298- CreatedAt: row.CreatedAt.Time,
299- }
300- if row.ParentID.Valid {
301- pid := entity.FolderID(row.ParentID.Int64)
302- folder.ParentID = &pid
303- }
304- return folder
305 }178 }
306 179
307-func foldersFromRows(rows []sqlc.FavoriteFolder) []*entity.Folder {180+func favoritesFromRows(rows []sqlc.Favorite) []*entity.Favorite {
308- folders := make([]*entity.Folder, len(rows))181+ favorites := make([]*entity.Favorite, len(rows))
309- for i, row := range rows {182+ for i := range rows {
310- folders[i] = folderFromRow(row)183+ favorites[i] = favoriteFromRow(rows[i], nil)
311 }184 }
312- return folders185+ return favorites
313 }186 }
314 187
315 // ==================== Tag Repository ====================188 // ==================== Tag Repository ====================
A internal/infrastructure/persistence/sqlite/favorite_repo_test.go +132 -0
@@ -0,0 +1,132 @@
1+package sqlite
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ _ "github.com/bnema/purego-sqlite/driver"
10+)
11+
12+func openFavoriteRepoTestDB(t *testing.T) *sql.DB {
13+ t.Helper()
14+ db, err := sql.Open("sqlite3", ":memory:")
15+ if err != nil {
16+ t.Fatalf("open sqlite: %v", err)
17+ }
18+ db.SetMaxOpenConns(1)
19+ t.Cleanup(func() { _ = db.Close() })
20+ if err := RunMigrations(context.Background(), db); err != nil {
21+ t.Fatalf("run migrations: %v", err)
22+ }
23+ return db
24+}
25+
26+func seedFavoriteWithTags(t *testing.T, db *sql.DB) (entity.FavoriteID, entity.FavoriteID, entity.TagID) {
27+ t.Helper()
28+ mustExec(t, db, `INSERT INTO favorites(id, url, title, shortcut_key, position) VALUES (1, 'https://one.example', 'One', 1, 1), (2, 'https://two.example', 'Two', NULL, 2)`)
29+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, 'research', '#111111'), (2, 'engines', '#222222')`)
30+ mustExec(t, db, `INSERT INTO favorite_tag_assignments(favorite_id, tag_id) VALUES (1, 1), (1, 2), (2, 1)`)
31+ return 1, 2, 1
32+}
33+
34+func TestFavoriteRepositoryGetAllHydratesTags(t *testing.T) {
35+ db := openFavoriteRepoTestDB(t)
36+ seedFavoriteWithTags(t, db)
37+ repo := NewFavoriteRepository(db)
38+
39+ favorites, err := repo.GetAll(context.Background())
40+ if err != nil {
41+ t.Fatalf("GetAll: %v", err)
42+ }
43+ if len(favorites) != 2 {
44+ t.Fatalf("expected 2 favorites, got %d", len(favorites))
45+ }
46+ assertTagNames(t, favorites[0], "engines", "research")
47+ assertTagNames(t, favorites[1], "research")
48+}
49+
50+func TestFavoriteRepositoryFindByIDHydratesTags(t *testing.T) {
51+ db := openFavoriteRepoTestDB(t)
52+ favID, _, _ := seedFavoriteWithTags(t, db)
53+ repo := NewFavoriteRepository(db)
54+
55+ fav, err := repo.FindByID(context.Background(), favID)
56+ if err != nil {
57+ t.Fatalf("FindByID: %v", err)
58+ }
59+ assertTagNames(t, fav, "engines", "research")
60+}
61+
62+func TestFavoriteRepositoryFindByURLHydratesTags(t *testing.T) {
63+ db := openFavoriteRepoTestDB(t)
64+ seedFavoriteWithTags(t, db)
65+ repo := NewFavoriteRepository(db)
66+
67+ fav, err := repo.FindByURL(context.Background(), "https://one.example")
68+ if err != nil {
69+ t.Fatalf("FindByURL: %v", err)
70+ }
71+ assertTagNames(t, fav, "engines", "research")
72+}
73+
74+func TestFavoriteRepositoryGetByTagHydratesTags(t *testing.T) {
75+ db := openFavoriteRepoTestDB(t)
76+ _, _, tagID := seedFavoriteWithTags(t, db)
77+ repo := NewFavoriteRepository(db)
78+
79+ favorites, err := repo.GetByTag(context.Background(), tagID)
80+ if err != nil {
81+ t.Fatalf("GetByTag: %v", err)
82+ }
83+ if len(favorites) != 2 {
84+ t.Fatalf("expected 2 tagged favorites, got %d", len(favorites))
85+ }
86+ assertTagNames(t, favorites[0], "engines", "research")
87+ assertTagNames(t, favorites[1], "research")
88+}
89+
90+func TestFavoriteRepositoryGetByShortcutHydratesTags(t *testing.T) {
91+ db := openFavoriteRepoTestDB(t)
92+ seedFavoriteWithTags(t, db)
93+ repo := NewFavoriteRepository(db)
94+
95+ fav, err := repo.GetByShortcut(context.Background(), 1)
96+ if err != nil {
97+ t.Fatalf("GetByShortcut: %v", err)
98+ }
99+ assertTagNames(t, fav, "engines", "research")
100+}
101+
102+func TestFavoriteRepositoryNoTagsHydratesEmptyTags(t *testing.T) {
103+ db := openFavoriteRepoTestDB(t)
104+ mustExec(t, db, `INSERT INTO favorites(id, url, title, position) VALUES (1, 'https://none.example', 'None', 1)`)
105+ repo := NewFavoriteRepository(db)
106+
107+ fav, err := repo.FindByID(context.Background(), 1)
108+ if err != nil {
109+ t.Fatalf("FindByID: %v", err)
110+ }
111+ if fav == nil {
112+ t.Fatal("expected favorite")
113+ }
114+ if fav.Tags == nil || len(fav.Tags) != 0 {
115+ t.Fatalf("expected non-nil empty Tags, got %#v", fav.Tags)
116+ }
117+}
118+
119+func assertTagNames(t *testing.T, fav *entity.Favorite, names ...string) {
120+ t.Helper()
121+ if fav == nil {
122+ t.Fatal("favorite is nil")
123+ }
124+ if len(fav.Tags) != len(names) {
125+ t.Fatalf("expected %d tags, got %d (%#v)", len(names), len(fav.Tags), fav.Tags)
126+ }
127+ for i, name := range names {
128+ if fav.Tags[i].Name != name {
129+ t.Fatalf("tag %d: expected %q, got %q", i, name, fav.Tags[i].Name)
130+ }
131+ }
132+}
@@ -0,0 +1,132 @@
1+package sqlite
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "testing"
7+
8+ "github.com/bnema/dumber/internal/domain/entity"
9+ _ "github.com/bnema/purego-sqlite/driver"
10+)
11+
12+func openFavoriteRepoTestDB(t *testing.T) *sql.DB {
13+ t.Helper()
14+ db, err := sql.Open("sqlite3", ":memory:")
15+ if err != nil {
16+ t.Fatalf("open sqlite: %v", err)
17+ }
18+ db.SetMaxOpenConns(1)
19+ t.Cleanup(func() { _ = db.Close() })
20+ if err := RunMigrations(context.Background(), db); err != nil {
21+ t.Fatalf("run migrations: %v", err)
22+ }
23+ return db
24+}
25+
26+func seedFavoriteWithTags(t *testing.T, db *sql.DB) (entity.FavoriteID, entity.FavoriteID, entity.TagID) {
27+ t.Helper()
28+ mustExec(t, db, `INSERT INTO favorites(id, url, title, shortcut_key, position) VALUES (1, 'https://one.example', 'One', 1, 1), (2, 'https://two.example', 'Two', NULL, 2)`)
29+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, 'research', '#111111'), (2, 'engines', '#222222')`)
30+ mustExec(t, db, `INSERT INTO favorite_tag_assignments(favorite_id, tag_id) VALUES (1, 1), (1, 2), (2, 1)`)
31+ return 1, 2, 1
32+}
33+
34+func TestFavoriteRepositoryGetAllHydratesTags(t *testing.T) {
35+ db := openFavoriteRepoTestDB(t)
36+ seedFavoriteWithTags(t, db)
37+ repo := NewFavoriteRepository(db)
38+
39+ favorites, err := repo.GetAll(context.Background())
40+ if err != nil {
41+ t.Fatalf("GetAll: %v", err)
42+ }
43+ if len(favorites) != 2 {
44+ t.Fatalf("expected 2 favorites, got %d", len(favorites))
45+ }
46+ assertTagNames(t, favorites[0], "engines", "research")
47+ assertTagNames(t, favorites[1], "research")
48+}
49+
50+func TestFavoriteRepositoryFindByIDHydratesTags(t *testing.T) {
51+ db := openFavoriteRepoTestDB(t)
52+ favID, _, _ := seedFavoriteWithTags(t, db)
53+ repo := NewFavoriteRepository(db)
54+
55+ fav, err := repo.FindByID(context.Background(), favID)
56+ if err != nil {
57+ t.Fatalf("FindByID: %v", err)
58+ }
59+ assertTagNames(t, fav, "engines", "research")
60+}
61+
62+func TestFavoriteRepositoryFindByURLHydratesTags(t *testing.T) {
63+ db := openFavoriteRepoTestDB(t)
64+ seedFavoriteWithTags(t, db)
65+ repo := NewFavoriteRepository(db)
66+
67+ fav, err := repo.FindByURL(context.Background(), "https://one.example")
68+ if err != nil {
69+ t.Fatalf("FindByURL: %v", err)
70+ }
71+ assertTagNames(t, fav, "engines", "research")
72+}
73+
74+func TestFavoriteRepositoryGetByTagHydratesTags(t *testing.T) {
75+ db := openFavoriteRepoTestDB(t)
76+ _, _, tagID := seedFavoriteWithTags(t, db)
77+ repo := NewFavoriteRepository(db)
78+
79+ favorites, err := repo.GetByTag(context.Background(), tagID)
80+ if err != nil {
81+ t.Fatalf("GetByTag: %v", err)
82+ }
83+ if len(favorites) != 2 {
84+ t.Fatalf("expected 2 tagged favorites, got %d", len(favorites))
85+ }
86+ assertTagNames(t, favorites[0], "engines", "research")
87+ assertTagNames(t, favorites[1], "research")
88+}
89+
90+func TestFavoriteRepositoryGetByShortcutHydratesTags(t *testing.T) {
91+ db := openFavoriteRepoTestDB(t)
92+ seedFavoriteWithTags(t, db)
93+ repo := NewFavoriteRepository(db)
94+
95+ fav, err := repo.GetByShortcut(context.Background(), 1)
96+ if err != nil {
97+ t.Fatalf("GetByShortcut: %v", err)
98+ }
99+ assertTagNames(t, fav, "engines", "research")
100+}
101+
102+func TestFavoriteRepositoryNoTagsHydratesEmptyTags(t *testing.T) {
103+ db := openFavoriteRepoTestDB(t)
104+ mustExec(t, db, `INSERT INTO favorites(id, url, title, position) VALUES (1, 'https://none.example', 'None', 1)`)
105+ repo := NewFavoriteRepository(db)
106+
107+ fav, err := repo.FindByID(context.Background(), 1)
108+ if err != nil {
109+ t.Fatalf("FindByID: %v", err)
110+ }
111+ if fav == nil {
112+ t.Fatal("expected favorite")
113+ }
114+ if fav.Tags == nil || len(fav.Tags) != 0 {
115+ t.Fatalf("expected non-nil empty Tags, got %#v", fav.Tags)
116+ }
117+}
118+
119+func assertTagNames(t *testing.T, fav *entity.Favorite, names ...string) {
120+ t.Helper()
121+ if fav == nil {
122+ t.Fatal("favorite is nil")
123+ }
124+ if len(fav.Tags) != len(names) {
125+ t.Fatalf("expected %d tags, got %d (%#v)", len(names), len(fav.Tags), fav.Tags)
126+ }
127+ for i, name := range names {
128+ if fav.Tags[i].Name != name {
129+ t.Fatalf("tag %d: expected %q, got %q", i, name, fav.Tags[i].Name)
130+ }
131+ }
132+}
A internal/infrastructure/persistence/sqlite/favorites_migration_test.go +116 -0
@@ -0,0 +1,116 @@
1+package sqlite
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "fmt"
7+ "testing"
8+
9+ _ "github.com/bnema/purego-sqlite/driver"
10+ "github.com/pressly/goose/v3"
11+)
12+
13+func openMigrationTestDB(t *testing.T) *sql.DB {
14+ t.Helper()
15+ db, err := sql.Open("sqlite3", ":memory:")
16+ if err != nil {
17+ t.Fatalf("open sqlite: %v", err)
18+ }
19+ db.SetMaxOpenConns(1)
20+ t.Cleanup(func() { _ = db.Close() })
21+ return db
22+}
23+
24+func runMigrationsUpTo(t *testing.T, db *sql.DB, version int64) {
25+ t.Helper()
26+ goose.SetBaseFS(embedMigrations)
27+ if err := goose.SetDialect("sqlite3"); err != nil {
28+ t.Fatalf("set dialect: %v", err)
29+ }
30+ goose.SetLogger(goose.NopLogger())
31+ if err := goose.UpTo(db, "migrations", version); err != nil {
32+ t.Fatalf("migrate to %d: %v", version, err)
33+ }
34+}
35+
36+func runAllMigrations(t *testing.T, db *sql.DB) {
37+ t.Helper()
38+ if err := RunMigrations(context.Background(), db); err != nil {
39+ t.Fatalf("run migrations: %v", err)
40+ }
41+}
42+
43+func countRows(t *testing.T, db *sql.DB, query string, args ...any) int {
44+ t.Helper()
45+ var count int
46+ if err := db.QueryRow(query, args...).Scan(&count); err != nil {
47+ t.Fatalf("count rows %q: %v", query, err)
48+ }
49+ return count
50+}
51+
52+func TestFavoritesTagsFirstMigrationFolderPathToTag(t *testing.T) {
53+ db := openMigrationTestDB(t)
54+ runMigrationsUpTo(t, db, 10)
55+
56+ mustExec(t, db, `INSERT INTO favorite_folders(id, name, parent_id) VALUES (1, 'Research', NULL), (2, 'Browser Engines', 1)`)
57+ mustExec(t, db, `INSERT INTO favorites(id, url, title, folder_id) VALUES (1, 'https://webkit.org', 'WebKit', 2)`)
58+
59+ runAllMigrations(t, db)
60+
61+ var tagID int64
62+ if err := db.QueryRow(`SELECT id FROM favorite_tags WHERE name = 'research-browser-engines'`).Scan(&tagID); err != nil {
63+ t.Fatalf("expected folder path tag: %v", err)
64+ }
65+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE favorite_id = 1 AND tag_id = ?`, tagID); got != 1 {
66+ t.Fatalf("expected assignment to folder path tag, got %d", got)
67+ }
68+}
69+
70+func TestFavoritesTagsFirstMigrationFolderTagCollisionSuffix(t *testing.T) {
71+ db := openMigrationTestDB(t)
72+ runMigrationsUpTo(t, db, 10)
73+
74+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, 'dev-go', '#111111')`)
75+ mustExec(t, db, `INSERT INTO favorite_folders(id, name, parent_id) VALUES (7, 'Dev', NULL), (8, 'Go', 7)`)
76+ mustExec(t, db, `INSERT INTO favorites(id, url, title, folder_id) VALUES (1, 'https://go.dev', 'Go', 8)`)
77+
78+ runAllMigrations(t, db)
79+
80+ var tagID int64
81+ if err := db.QueryRow(`SELECT id FROM favorite_tags WHERE name = 'dev-go-folder-8'`).Scan(&tagID); err != nil {
82+ t.Fatalf("expected suffixed collision tag: %v", err)
83+ }
84+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE favorite_id = 1 AND tag_id = ?`, tagID); got != 1 {
85+ t.Fatalf("expected assignment to suffixed tag, got %d", got)
86+ }
87+}
88+
89+func TestFavoritesTagsFirstMigrationMergesFoldedDuplicateTags(t *testing.T) {
90+ db := openMigrationTestDB(t)
91+ runMigrationsUpTo(t, db, 10)
92+
93+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, ' Dev-Go ', '#111111'), (2, 'dev-go', '#222222')`)
94+ mustExec(t, db, `INSERT INTO favorites(id, url, title) VALUES (1, 'https://one.example', 'One'), (2, 'https://two.example', 'Two')`)
95+ mustExec(t, db, `INSERT INTO favorite_tag_assignments(favorite_id, tag_id) VALUES (1, 1), (2, 2)`)
96+
97+ runAllMigrations(t, db)
98+
99+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tags WHERE lower(trim(name)) = 'dev-go'`); got != 1 {
100+ t.Fatalf("expected one folded dev-go tag, got %d", got)
101+ }
102+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE tag_id = 1`); got != 2 {
103+ t.Fatalf("expected assignments moved to canonical tag 1, got %d", got)
104+ }
105+ mustExec(t, db, `INSERT INTO favorite_tags(name, color) VALUES ('another', '#333333')`)
106+ if _, err := db.Exec(`INSERT INTO favorite_tags(name, color) VALUES (' DEV-GO ', '#444444')`); err == nil {
107+ t.Fatal("expected folded unique index to reject duplicate tag")
108+ }
109+}
110+
111+func mustExec(t *testing.T, db *sql.DB, query string, args ...any) {
112+ t.Helper()
113+ if _, err := db.Exec(query, args...); err != nil {
114+ t.Fatalf("exec %s: %v", fmt.Sprintf(query, args...), err)
115+ }
116+}
@@ -0,0 +1,116 @@
1+package sqlite
2+
3+import (
4+ "context"
5+ "database/sql"
6+ "fmt"
7+ "testing"
8+
9+ _ "github.com/bnema/purego-sqlite/driver"
10+ "github.com/pressly/goose/v3"
11+)
12+
13+func openMigrationTestDB(t *testing.T) *sql.DB {
14+ t.Helper()
15+ db, err := sql.Open("sqlite3", ":memory:")
16+ if err != nil {
17+ t.Fatalf("open sqlite: %v", err)
18+ }
19+ db.SetMaxOpenConns(1)
20+ t.Cleanup(func() { _ = db.Close() })
21+ return db
22+}
23+
24+func runMigrationsUpTo(t *testing.T, db *sql.DB, version int64) {
25+ t.Helper()
26+ goose.SetBaseFS(embedMigrations)
27+ if err := goose.SetDialect("sqlite3"); err != nil {
28+ t.Fatalf("set dialect: %v", err)
29+ }
30+ goose.SetLogger(goose.NopLogger())
31+ if err := goose.UpTo(db, "migrations", version); err != nil {
32+ t.Fatalf("migrate to %d: %v", version, err)
33+ }
34+}
35+
36+func runAllMigrations(t *testing.T, db *sql.DB) {
37+ t.Helper()
38+ if err := RunMigrations(context.Background(), db); err != nil {
39+ t.Fatalf("run migrations: %v", err)
40+ }
41+}
42+
43+func countRows(t *testing.T, db *sql.DB, query string, args ...any) int {
44+ t.Helper()
45+ var count int
46+ if err := db.QueryRow(query, args...).Scan(&count); err != nil {
47+ t.Fatalf("count rows %q: %v", query, err)
48+ }
49+ return count
50+}
51+
52+func TestFavoritesTagsFirstMigrationFolderPathToTag(t *testing.T) {
53+ db := openMigrationTestDB(t)
54+ runMigrationsUpTo(t, db, 10)
55+
56+ mustExec(t, db, `INSERT INTO favorite_folders(id, name, parent_id) VALUES (1, 'Research', NULL), (2, 'Browser Engines', 1)`)
57+ mustExec(t, db, `INSERT INTO favorites(id, url, title, folder_id) VALUES (1, 'https://webkit.org', 'WebKit', 2)`)
58+
59+ runAllMigrations(t, db)
60+
61+ var tagID int64
62+ if err := db.QueryRow(`SELECT id FROM favorite_tags WHERE name = 'research-browser-engines'`).Scan(&tagID); err != nil {
63+ t.Fatalf("expected folder path tag: %v", err)
64+ }
65+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE favorite_id = 1 AND tag_id = ?`, tagID); got != 1 {
66+ t.Fatalf("expected assignment to folder path tag, got %d", got)
67+ }
68+}
69+
70+func TestFavoritesTagsFirstMigrationFolderTagCollisionSuffix(t *testing.T) {
71+ db := openMigrationTestDB(t)
72+ runMigrationsUpTo(t, db, 10)
73+
74+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, 'dev-go', '#111111')`)
75+ mustExec(t, db, `INSERT INTO favorite_folders(id, name, parent_id) VALUES (7, 'Dev', NULL), (8, 'Go', 7)`)
76+ mustExec(t, db, `INSERT INTO favorites(id, url, title, folder_id) VALUES (1, 'https://go.dev', 'Go', 8)`)
77+
78+ runAllMigrations(t, db)
79+
80+ var tagID int64
81+ if err := db.QueryRow(`SELECT id FROM favorite_tags WHERE name = 'dev-go-folder-8'`).Scan(&tagID); err != nil {
82+ t.Fatalf("expected suffixed collision tag: %v", err)
83+ }
84+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE favorite_id = 1 AND tag_id = ?`, tagID); got != 1 {
85+ t.Fatalf("expected assignment to suffixed tag, got %d", got)
86+ }
87+}
88+
89+func TestFavoritesTagsFirstMigrationMergesFoldedDuplicateTags(t *testing.T) {
90+ db := openMigrationTestDB(t)
91+ runMigrationsUpTo(t, db, 10)
92+
93+ mustExec(t, db, `INSERT INTO favorite_tags(id, name, color) VALUES (1, ' Dev-Go ', '#111111'), (2, 'dev-go', '#222222')`)
94+ mustExec(t, db, `INSERT INTO favorites(id, url, title) VALUES (1, 'https://one.example', 'One'), (2, 'https://two.example', 'Two')`)
95+ mustExec(t, db, `INSERT INTO favorite_tag_assignments(favorite_id, tag_id) VALUES (1, 1), (2, 2)`)
96+
97+ runAllMigrations(t, db)
98+
99+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tags WHERE lower(trim(name)) = 'dev-go'`); got != 1 {
100+ t.Fatalf("expected one folded dev-go tag, got %d", got)
101+ }
102+ if got := countRows(t, db, `SELECT COUNT(*) FROM favorite_tag_assignments WHERE tag_id = 1`); got != 2 {
103+ t.Fatalf("expected assignments moved to canonical tag 1, got %d", got)
104+ }
105+ mustExec(t, db, `INSERT INTO favorite_tags(name, color) VALUES ('another', '#333333')`)
106+ if _, err := db.Exec(`INSERT INTO favorite_tags(name, color) VALUES (' DEV-GO ', '#444444')`); err == nil {
107+ t.Fatal("expected folded unique index to reject duplicate tag")
108+ }
109+}
110+
111+func mustExec(t *testing.T, db *sql.DB, query string, args ...any) {
112+ t.Helper()
113+ if _, err := db.Exec(query, args...); err != nil {
114+ t.Fatalf("exec %s: %v", fmt.Sprintf(query, args...), err)
115+ }
116+}
M internal/infrastructure/persistence/sqlite/lazy_repos.go +2 -78
@@ -357,11 +357,11 @@ func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite
357357 return r.repo.GetAll(ctx)
358358 }
359359
360-func (r *LazyFavoriteRepository) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {
360+func (r *LazyFavoriteRepository) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
361361 if err := r.init(ctx); err != nil {
362362 return nil, err
363363 }
364- return r.repo.GetByFolder(ctx, folderID)
364+ return r.repo.GetByTag(ctx, tagID)
365365 }
366366
367367 func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
@@ -378,13 +378,6 @@ func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.F
378378 return r.repo.UpdatePosition(ctx, id, position)
379379 }
380380
381-func (r *LazyFavoriteRepository) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
382- if err := r.init(ctx); err != nil {
383- return err
384- }
385- return r.repo.SetFolder(ctx, id, folderID)
386-}
387-
388381 func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
389382 if err := r.init(ctx); err != nil {
390383 return err
@@ -399,73 +392,6 @@ func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteI
399392 return r.repo.Delete(ctx, id)
400393 }
401394
402-// LazyFolderRepository wraps a folder repository with lazy database initialization.
403-type LazyFolderRepository struct {
404- provider port.DatabaseProvider
405- repo repository.FolderRepository
406- once sync.Once
407- initErr error
408-}
409-
410-// NewLazyFolderRepository creates a lazy-loading folder repository.
411-func NewLazyFolderRepository(provider port.DatabaseProvider) repository.FolderRepository {
412- return &LazyFolderRepository{provider: provider}
413-}
414-
415-func (r *LazyFolderRepository) init(ctx context.Context) error {
416- r.once.Do(func() {
417- db, err := r.provider.DB(ctx)
418- if err != nil {
419- r.initErr = err
420- return
421- }
422- r.repo = NewFolderRepository(db)
423- })
424- return r.initErr
425-}
426-
427-func (r *LazyFolderRepository) Save(ctx context.Context, folder *entity.Folder) error {
428- if err := r.init(ctx); err != nil {
429- return err
430- }
431- return r.repo.Save(ctx, folder)
432-}
433-
434-func (r *LazyFolderRepository) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
435- if err := r.init(ctx); err != nil {
436- return nil, err
437- }
438- return r.repo.FindByID(ctx, id)
439-}
440-
441-func (r *LazyFolderRepository) GetAll(ctx context.Context) ([]*entity.Folder, error) {
442- if err := r.init(ctx); err != nil {
443- return nil, err
444- }
445- return r.repo.GetAll(ctx)
446-}
447-
448-func (r *LazyFolderRepository) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
449- if err := r.init(ctx); err != nil {
450- return nil, err
451- }
452- return r.repo.GetChildren(ctx, parentID)
453-}
454-
455-func (r *LazyFolderRepository) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
456- if err := r.init(ctx); err != nil {
457- return err
458- }
459- return r.repo.UpdatePosition(ctx, id, position)
460-}
461-
462-func (r *LazyFolderRepository) Delete(ctx context.Context, id entity.FolderID) error {
463- if err := r.init(ctx); err != nil {
464- return err
465- }
466- return r.repo.Delete(ctx, id)
467-}
468-
469395 // LazyTagRepository wraps a tag repository with lazy database initialization.
470396 type LazyTagRepository struct {
471397 provider port.DatabaseProvider
@@ -798,7 +724,6 @@ func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string,
798724 type LazyRepositories struct {
799725 History repository.HistoryRepository
800726 Favorite repository.FavoriteRepository
801- Folder repository.FolderRepository
802727 Tag repository.TagRepository
803728 Zoom repository.ZoomRepository
804729 Session repository.SessionRepository
@@ -812,7 +737,6 @@ func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
812737 return &LazyRepositories{
813738 History: NewLazyHistoryRepository(provider),
814739 Favorite: NewLazyFavoriteRepository(provider),
815- Folder: NewLazyFolderRepository(provider),
816740 Tag: NewLazyTagRepository(provider),
817741 Zoom: NewLazyZoomRepository(provider),
818742 Session: NewLazySessionRepository(provider),
@@ -357,11 +357,11 @@ func (r *LazyFavoriteRepository) GetAll(ctx context.Context) ([]*entity.Favorite
357 return r.repo.GetAll(ctx)357 return r.repo.GetAll(ctx)
358 }358 }
359 359
360-func (r *LazyFavoriteRepository) GetByFolder(ctx context.Context, folderID *entity.FolderID) ([]*entity.Favorite, error) {360+func (r *LazyFavoriteRepository) GetByTag(ctx context.Context, tagID entity.TagID) ([]*entity.Favorite, error) {
361 if err := r.init(ctx); err != nil {361 if err := r.init(ctx); err != nil {
362 return nil, err362 return nil, err
363 }363 }
364- return r.repo.GetByFolder(ctx, folderID)364+ return r.repo.GetByTag(ctx, tagID)
365 }365 }
366 366
367 func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {367 func (r *LazyFavoriteRepository) GetByShortcut(ctx context.Context, key int) (*entity.Favorite, error) {
@@ -378,13 +378,6 @@ func (r *LazyFavoriteRepository) UpdatePosition(ctx context.Context, id entity.F
378 return r.repo.UpdatePosition(ctx, id, position)378 return r.repo.UpdatePosition(ctx, id, position)
379 }379 }
380 380
381-func (r *LazyFavoriteRepository) SetFolder(ctx context.Context, id entity.FavoriteID, folderID *entity.FolderID) error {
382- if err := r.init(ctx); err != nil {
383- return err
384- }
385- return r.repo.SetFolder(ctx, id, folderID)
386-}
387-
388 func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {381 func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error {
389 if err := r.init(ctx); err != nil {382 if err := r.init(ctx); err != nil {
390 return err383 return err
@@ -399,73 +392,6 @@ func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteI
399 return r.repo.Delete(ctx, id)392 return r.repo.Delete(ctx, id)
400 }393 }
401 394
402-// LazyFolderRepository wraps a folder repository with lazy database initialization.
403-type LazyFolderRepository struct {
404- provider port.DatabaseProvider
405- repo repository.FolderRepository
406- once sync.Once
407- initErr error
408-}
409-
410-// NewLazyFolderRepository creates a lazy-loading folder repository.
411-func NewLazyFolderRepository(provider port.DatabaseProvider) repository.FolderRepository {
412- return &LazyFolderRepository{provider: provider}
413-}
414-
415-func (r *LazyFolderRepository) init(ctx context.Context) error {
416- r.once.Do(func() {
417- db, err := r.provider.DB(ctx)
418- if err != nil {
419- r.initErr = err
420- return
421- }
422- r.repo = NewFolderRepository(db)
423- })
424- return r.initErr
425-}
426-
427-func (r *LazyFolderRepository) Save(ctx context.Context, folder *entity.Folder) error {
428- if err := r.init(ctx); err != nil {
429- return err
430- }
431- return r.repo.Save(ctx, folder)
432-}
433-
434-func (r *LazyFolderRepository) FindByID(ctx context.Context, id entity.FolderID) (*entity.Folder, error) {
435- if err := r.init(ctx); err != nil {
436- return nil, err
437- }
438- return r.repo.FindByID(ctx, id)
439-}
440-
441-func (r *LazyFolderRepository) GetAll(ctx context.Context) ([]*entity.Folder, error) {
442- if err := r.init(ctx); err != nil {
443- return nil, err
444- }
445- return r.repo.GetAll(ctx)
446-}
447-
448-func (r *LazyFolderRepository) GetChildren(ctx context.Context, parentID *entity.FolderID) ([]*entity.Folder, error) {
449- if err := r.init(ctx); err != nil {
450- return nil, err
451- }
452- return r.repo.GetChildren(ctx, parentID)
453-}
454-
455-func (r *LazyFolderRepository) UpdatePosition(ctx context.Context, id entity.FolderID, position int) error {
456- if err := r.init(ctx); err != nil {
457- return err
458- }
459- return r.repo.UpdatePosition(ctx, id, position)
460-}
461-
462-func (r *LazyFolderRepository) Delete(ctx context.Context, id entity.FolderID) error {
463- if err := r.init(ctx); err != nil {
464- return err
465- }
466- return r.repo.Delete(ctx, id)
467-}
468-
469 // LazyTagRepository wraps a tag repository with lazy database initialization.395 // LazyTagRepository wraps a tag repository with lazy database initialization.
470 type LazyTagRepository struct {396 type LazyTagRepository struct {
471 provider port.DatabaseProvider397 provider port.DatabaseProvider
@@ -798,7 +724,6 @@ func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string,
798 type LazyRepositories struct {724 type LazyRepositories struct {
799 History repository.HistoryRepository725 History repository.HistoryRepository
800 Favorite repository.FavoriteRepository726 Favorite repository.FavoriteRepository
801- Folder repository.FolderRepository
802 Tag repository.TagRepository727 Tag repository.TagRepository
803 Zoom repository.ZoomRepository728 Zoom repository.ZoomRepository
804 Session repository.SessionRepository729 Session repository.SessionRepository
@@ -812,7 +737,6 @@ func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories {
812 return &LazyRepositories{737 return &LazyRepositories{
813 History: NewLazyHistoryRepository(provider),738 History: NewLazyHistoryRepository(provider),
814 Favorite: NewLazyFavoriteRepository(provider),739 Favorite: NewLazyFavoriteRepository(provider),
815- Folder: NewLazyFolderRepository(provider),
816 Tag: NewLazyTagRepository(provider),740 Tag: NewLazyTagRepository(provider),
817 Zoom: NewLazyZoomRepository(provider),741 Zoom: NewLazyZoomRepository(provider),
818 Session: NewLazySessionRepository(provider),742 Session: NewLazySessionRepository(provider),
A internal/infrastructure/persistence/sqlite/migrations/011_favorites_tags_first.sql +86 -0
@@ -0,0 +1,86 @@
1+-- +goose Up
2+-- Convert favorite organization from folders to tags.
3+
4+-- Reconcile duplicate tags by folded name before adding folded uniqueness.
5+CREATE TEMP TABLE _favorite_tag_canonical AS
6+SELECT lower(trim(name)) AS folded_name, MIN(id) AS canonical_id
7+FROM favorite_tags
8+GROUP BY lower(trim(name));
9+
10+INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id)
11+SELECT fta.favorite_id, c.canonical_id
12+FROM favorite_tag_assignments fta
13+JOIN favorite_tags t ON t.id = fta.tag_id
14+JOIN _favorite_tag_canonical c ON c.folded_name = lower(trim(t.name))
15+WHERE fta.tag_id <> c.canonical_id;
16+
17+DELETE FROM favorite_tag_assignments
18+WHERE tag_id NOT IN (SELECT canonical_id FROM _favorite_tag_canonical);
19+
20+DELETE FROM favorite_tags
21+WHERE id NOT IN (SELECT canonical_id FROM _favorite_tag_canonical);
22+
23+DROP TABLE _favorite_tag_canonical;
24+
25+CREATE UNIQUE INDEX IF NOT EXISTS idx_favorite_tags_name_folded ON favorite_tags(lower(trim(name)));
26+
27+-- Build deterministic folder-path slugs. Keep folder tables/columns for compatibility,
28+-- but materialize every current folder assignment as a tag assignment.
29+CREATE TEMP TABLE _folder_tag_map (
30+ folder_id INTEGER PRIMARY KEY,
31+ tag_name TEXT NOT NULL UNIQUE
32+);
33+
34+WITH RECURSIVE folder_paths(id, parent_id, path) AS (
35+ SELECT id, parent_id, trim(name)
36+ FROM favorite_folders
37+ WHERE parent_id IS NULL
38+ UNION ALL
39+ SELECT child.id, child.parent_id, folder_paths.path || '-' || trim(child.name)
40+ FROM favorite_folders child
41+ JOIN folder_paths ON child.parent_id = folder_paths.id
42+), folder_slugs AS (
43+ SELECT
44+ id,
45+ lower(
46+ trim(
47+ replace(replace(replace(replace(replace(path, '/', '-'), '\\', '-'), ' ', '-'), '_', '-'), '.', '-'),
48+ '-'
49+ )
50+ ) AS base_slug
51+ FROM folder_paths
52+), resolved AS (
53+ SELECT
54+ id,
55+ CASE
56+ WHEN base_slug = '' THEN 'folder-' || id
57+ WHEN EXISTS (
58+ SELECT 1 FROM favorite_tags t
59+ WHERE lower(trim(t.name)) = lower(trim(base_slug))
60+ ) OR EXISTS (
61+ SELECT 1 FROM folder_slugs earlier
62+ WHERE earlier.id < folder_slugs.id
63+ AND lower(trim(earlier.base_slug)) = lower(trim(folder_slugs.base_slug))
64+ ) THEN base_slug || '-folder-' || id
65+ ELSE base_slug
66+ END AS tag_name
67+ FROM folder_slugs
68+)
69+INSERT INTO _folder_tag_map(folder_id, tag_name)
70+SELECT id, tag_name FROM resolved;
71+
72+INSERT OR IGNORE INTO favorite_tags(name, color)
73+SELECT tag_name, '#808080'
74+FROM _folder_tag_map;
75+
76+INSERT OR IGNORE INTO favorite_tag_assignments(favorite_id, tag_id)
77+SELECT f.id, t.id
78+FROM favorites f
79+JOIN _folder_tag_map ftm ON ftm.folder_id = f.folder_id
80+JOIN favorite_tags t ON lower(trim(t.name)) = lower(trim(ftm.tag_name))
81+WHERE f.folder_id IS NOT NULL;
82+
83+DROP TABLE _folder_tag_map;
84+
85+-- +goose Down
86+DROP INDEX IF EXISTS idx_favorite_tags_name_folded;
@@ -0,0 +1,86 @@
1+-- +goose Up
2+-- Convert favorite organization from folders to tags.
3+
4+-- Reconcile duplicate tags by folded name before adding folded uniqueness.
5+CREATE TEMP TABLE _favorite_tag_canonical AS
6+SELECT lower(trim(name)) AS folded_name, MIN(id) AS canonical_id
7+FROM favorite_tags
8+GROUP BY lower(trim(name));
9+
10+INSERT OR IGNORE INTO favorite_tag_assignments (favorite_id, tag_id)
11+SELECT fta.favorite_id, c.canonical_id
12+FROM favorite_tag_assignments fta
13+JOIN favorite_tags t ON t.id = fta.tag_id
14+JOIN _favorite_tag_canonical c ON c.folded_name = lower(trim(t.name))
15+WHERE fta.tag_id <> c.canonical_id;
16+
17+DELETE FROM favorite_tag_assignments
18+WHERE tag_id NOT IN (SELECT canonical_id FROM _favorite_tag_canonical);
19+
20+DELETE FROM favorite_tags
21+WHERE id NOT IN (SELECT canonical_id FROM _favorite_tag_canonical);
22+
23+DROP TABLE _favorite_tag_canonical;
24+
25+CREATE UNIQUE INDEX IF NOT EXISTS idx_favorite_tags_name_folded ON favorite_tags(lower(trim(name)));
26+
27+-- Build deterministic folder-path slugs. Keep folder tables/columns for compatibility,
28+-- but materialize every current folder assignment as a tag assignment.
29+CREATE TEMP TABLE _folder_tag_map (
30+ folder_id INTEGER PRIMARY KEY,
31+ tag_name TEXT NOT NULL UNIQUE
32+);
33+
34+WITH RECURSIVE folder_paths(id, parent_id, path) AS (
35+ SELECT id, parent_id, trim(name)
36+ FROM favorite_folders
37+ WHERE parent_id IS NULL
38+ UNION ALL
39+ SELECT child.id, child.parent_id, folder_paths.path || '-' || trim(child.name)
40+ FROM favorite_folders child
41+ JOIN folder_paths ON child.parent_id = folder_paths.id
42+), folder_slugs AS (
43+ SELECT
44+ id,
45+ lower(
46+ trim(
47+ replace(replace(replace(replace(replace(path, '/', '-'), '\\', '-'), ' ', '-'), '_', '-'), '.', '-'),
48+ '-'
49+ )
50+ ) AS base_slug
51+ FROM folder_paths
52+), resolved AS (
53+ SELECT
54+ id,
55+ CASE
56+ WHEN base_slug = '' THEN 'folder-' || id
57+ WHEN EXISTS (
58+ SELECT 1 FROM favorite_tags t
59+ WHERE lower(trim(t.name)) = lower(trim(base_slug))
60+ ) OR EXISTS (
61+ SELECT 1 FROM folder_slugs earlier
62+ WHERE earlier.id < folder_slugs.id
63+ AND lower(trim(earlier.base_slug)) = lower(trim(folder_slugs.base_slug))
64+ ) THEN base_slug || '-folder-' || id
65+ ELSE base_slug
66+ END AS tag_name
67+ FROM folder_slugs
68+)
69+INSERT INTO _folder_tag_map(folder_id, tag_name)
70+SELECT id, tag_name FROM resolved;
71+
72+INSERT OR IGNORE INTO favorite_tags(name, color)
73+SELECT tag_name, '#808080'
74+FROM _folder_tag_map;
75+
76+INSERT OR IGNORE INTO favorite_tag_assignments(favorite_id, tag_id)
77+SELECT f.id, t.id
78+FROM favorites f
79+JOIN _folder_tag_map ftm ON ftm.folder_id = f.folder_id
80+JOIN favorite_tags t ON lower(trim(t.name)) = lower(trim(ftm.tag_name))
81+WHERE f.folder_id IS NOT NULL;
82+
83+DROP TABLE _folder_tag_map;
84+
85+-- +goose Down
86+DROP INDEX IF EXISTS idx_favorite_tags_name_folded;
M internal/infrastructure/persistence/sqlite/queries/favorites.sql +7 -10
@@ -7,18 +7,18 @@ SELECT * FROM favorites WHERE id = ? LIMIT 1;
77 -- name: GetFavoriteByURL :one
88 SELECT * FROM favorites WHERE url = ? LIMIT 1;
99
10--- name: GetFavoritesByFolder :many
11-SELECT * FROM favorites WHERE folder_id = ? ORDER BY position ASC;
12-
13--- name: GetFavoritesWithoutFolder :many
14-SELECT * FROM favorites WHERE folder_id IS NULL ORDER BY position ASC;
10+-- name: GetFavoritesByTag :many
11+SELECT f.* FROM favorites f
12+INNER JOIN favorite_tag_assignments fta ON f.id = fta.favorite_id
13+WHERE fta.tag_id = ?
14+ORDER BY f.position ASC;
1515
1616 -- name: GetFavoriteByShortcut :one
1717 SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1;
1818
1919 -- name: CreateFavorite :one
20-INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at)
21-VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
20+INSERT INTO favorites (url, title, favicon_url, position, created_at, updated_at)
21+VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
2222 RETURNING *;
2323
2424 -- name: UpdateFavorite :exec
@@ -29,9 +29,6 @@ WHERE id = ?;
2929 -- name: UpdateFavoritePosition :exec
3030 UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
3131
32--- name: SetFavoriteFolder :exec
33-UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
34-
3532 -- name: SetFavoriteShortcut :exec
3633 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
3734
@@ -7,18 +7,18 @@ SELECT * FROM favorites WHERE id = ? LIMIT 1;
7 -- name: GetFavoriteByURL :one7 -- name: GetFavoriteByURL :one
8 SELECT * FROM favorites WHERE url = ? LIMIT 1;8 SELECT * FROM favorites WHERE url = ? LIMIT 1;
9 9
10--- name: GetFavoritesByFolder :many10+-- name: GetFavoritesByTag :many
11-SELECT * FROM favorites WHERE folder_id = ? ORDER BY position ASC;11+SELECT f.* FROM favorites f
12-12+INNER JOIN favorite_tag_assignments fta ON f.id = fta.favorite_id
13--- name: GetFavoritesWithoutFolder :many13+WHERE fta.tag_id = ?
14-SELECT * FROM favorites WHERE folder_id IS NULL ORDER BY position ASC;14+ORDER BY f.position ASC;
15 15
16 -- name: GetFavoriteByShortcut :one16 -- name: GetFavoriteByShortcut :one
17 SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1;17 SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1;
18 18
19 -- name: CreateFavorite :one19 -- name: CreateFavorite :one
20-INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at)20+INSERT INTO favorites (url, title, favicon_url, position, created_at, updated_at)
21-VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)21+VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
22 RETURNING *;22 RETURNING *;
23 23
24 -- name: UpdateFavorite :exec24 -- name: UpdateFavorite :exec
@@ -29,9 +29,6 @@ WHERE id = ?;
29 -- name: UpdateFavoritePosition :exec29 -- name: UpdateFavoritePosition :exec
30 UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;30 UPDATE favorites SET position = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
31 31
32--- name: SetFavoriteFolder :exec
33-UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
34-
35 -- name: SetFavoriteShortcut :exec32 -- name: SetFavoriteShortcut :exec
36 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;33 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?;
37 34
D internal/infrastructure/persistence/sqlite/queries/folders.sql +0 -25
@@ -1,25 +0,0 @@
1--- name: GetAllFolders :many
2-SELECT * FROM favorite_folders ORDER BY position ASC;
3-
4--- name: GetFolderByID :one
5-SELECT * FROM favorite_folders WHERE id = ? LIMIT 1;
6-
7--- name: GetChildFolders :many
8-SELECT * FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC;
9-
10--- name: GetRootFolders :many
11-SELECT * FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC;
12-
13--- name: CreateFolder :one
14-INSERT INTO favorite_folders (name, icon, parent_id, position, created_at)
15-VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP)
16-RETURNING *;
17-
18--- name: UpdateFolder :exec
19-UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?;
20-
21--- name: UpdateFolderPosition :exec
22-UPDATE favorite_folders SET position = ? WHERE id = ?;
23-
24--- name: DeleteFolder :exec
25-DELETE FROM favorite_folders WHERE id = ?;
@@ -1,25 +0,0 @@
1--- name: GetAllFolders :many
2-SELECT * FROM favorite_folders ORDER BY position ASC;
3-
4--- name: GetFolderByID :one
5-SELECT * FROM favorite_folders WHERE id = ? LIMIT 1;
6-
7--- name: GetChildFolders :many
8-SELECT * FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC;
9-
10--- name: GetRootFolders :many
11-SELECT * FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC;
12-
13--- name: CreateFolder :one
14-INSERT INTO favorite_folders (name, icon, parent_id, position, created_at)
15-VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP)
16-RETURNING *;
17-
18--- name: UpdateFolder :exec
19-UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?;
20-
21--- name: UpdateFolderPosition :exec
22-UPDATE favorite_folders SET position = ? WHERE id = ?;
23-
24--- name: DeleteFolder :exec
25-DELETE FROM favorite_folders WHERE id = ?;
M internal/infrastructure/persistence/sqlite/queries/tags.sql +7 -1
@@ -5,7 +5,7 @@ SELECT * FROM favorite_tags ORDER BY name ASC;
55 SELECT * FROM favorite_tags WHERE id = ? LIMIT 1;
66
77 -- name: GetTagByName :one
8-SELECT * FROM favorite_tags WHERE name = ? LIMIT 1;
8+SELECT * FROM favorite_tags WHERE lower(trim(name)) = lower(trim(?)) LIMIT 1;
99
1010 -- name: CreateTag :one
1111 INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *;
@@ -27,3 +27,9 @@ SELECT t.* FROM favorite_tags t
2727 INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
2828 WHERE fta.favorite_id = ?
2929 ORDER BY t.name ASC;
30+
31+-- name: GetTagsForFavorites :many
32+SELECT sqlc.embed(t), fta.favorite_id FROM favorite_tags t
33+INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
34+WHERE fta.favorite_id IN (sqlc.slice('favorite_ids'))
35+ORDER BY fta.favorite_id ASC, t.name ASC;
@@ -5,7 +5,7 @@ SELECT * FROM favorite_tags ORDER BY name ASC;
5 SELECT * FROM favorite_tags WHERE id = ? LIMIT 1;5 SELECT * FROM favorite_tags WHERE id = ? LIMIT 1;
6 6
7 -- name: GetTagByName :one7 -- name: GetTagByName :one
8-SELECT * FROM favorite_tags WHERE name = ? LIMIT 1;8+SELECT * FROM favorite_tags WHERE lower(trim(name)) = lower(trim(?)) LIMIT 1;
9 9
10 -- name: CreateTag :one10 -- name: CreateTag :one
11 INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *;11 INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *;
@@ -27,3 +27,9 @@ SELECT t.* FROM favorite_tags t
27 INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id27 INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
28 WHERE fta.favorite_id = ?28 WHERE fta.favorite_id = ?
29 ORDER BY t.name ASC;29 ORDER BY t.name ASC;
30+
31+-- name: GetTagsForFavorites :many
32+SELECT sqlc.embed(t), fta.favorite_id FROM favorite_tags t
33+INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
34+WHERE fta.favorite_id IN (sqlc.slice('favorite_ids'))
35+ORDER BY fta.favorite_id ASC, t.name ASC;
M internal/infrastructure/persistence/sqlite/sqlc/favorites.sql.go +10 -64
@@ -11,8 +11,8 @@ import (
1111 )
1212
1313 const CreateFavorite = `-- name: CreateFavorite :one
14-INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at)
15-VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
14+INSERT INTO favorites (url, title, favicon_url, position, created_at, updated_at)
15+VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
1616 RETURNING id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at
1717 `
1818
@@ -20,16 +20,10 @@ type CreateFavoriteParams struct {
2020 Url string `json:"url"`
2121 Title sql.NullString `json:"title"`
2222 FaviconUrl sql.NullString `json:"favicon_url"`
23- FolderID sql.NullInt64 `json:"folder_id"`
2423 }
2524
2625 func (q *Queries) CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) {
27- row := q.db.QueryRowContext(ctx, CreateFavorite,
28- arg.Url,
29- arg.Title,
30- arg.FaviconUrl,
31- arg.FolderID,
32- )
26+ row := q.db.QueryRowContext(ctx, CreateFavorite, arg.Url, arg.Title, arg.FaviconUrl)
3327 var i Favorite
3428 err := row.Scan(
3529 &i.ID,
@@ -154,12 +148,15 @@ func (q *Queries) GetFavoriteByURL(ctx context.Context, url string) (Favorite, e
154148 return i, err
155149 }
156150
157-const GetFavoritesByFolder = `-- name: GetFavoritesByFolder :many
158-SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id = ? ORDER BY position ASC
151+const GetFavoritesByTag = `-- name: GetFavoritesByTag :many
152+SELECT f.id, f.url, f.title, f.favicon_url, f.folder_id, f.shortcut_key, f.position, f.created_at, f.updated_at FROM favorites f
153+INNER JOIN favorite_tag_assignments fta ON f.id = fta.favorite_id
154+WHERE fta.tag_id = ?
155+ORDER BY f.position ASC
159156 `
160157
161-func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) {
162- rows, err := q.db.QueryContext(ctx, GetFavoritesByFolder, folderID)
158+func (q *Queries) GetFavoritesByTag(ctx context.Context, tagID int64) ([]Favorite, error) {
159+ rows, err := q.db.QueryContext(ctx, GetFavoritesByTag, tagID)
163160 if err != nil {
164161 return nil, err
165162 }
@@ -191,57 +188,6 @@ func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt
191188 return items, nil
192189 }
193190
194-const GetFavoritesWithoutFolder = `-- name: GetFavoritesWithoutFolder :many
195-SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id IS NULL ORDER BY position ASC
196-`
197-
198-func (q *Queries) GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) {
199- rows, err := q.db.QueryContext(ctx, GetFavoritesWithoutFolder)
200- if err != nil {
201- return nil, err
202- }
203- defer rows.Close()
204- items := []Favorite{}
205- for rows.Next() {
206- var i Favorite
207- if err := rows.Scan(
208- &i.ID,
209- &i.Url,
210- &i.Title,
211- &i.FaviconUrl,
212- &i.FolderID,
213- &i.ShortcutKey,
214- &i.Position,
215- &i.CreatedAt,
216- &i.UpdatedAt,
217- ); err != nil {
218- return nil, err
219- }
220- items = append(items, i)
221- }
222- if err := rows.Close(); err != nil {
223- return nil, err
224- }
225- if err := rows.Err(); err != nil {
226- return nil, err
227- }
228- return items, nil
229-}
230-
231-const SetFavoriteFolder = `-- name: SetFavoriteFolder :exec
232-UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
233-`
234-
235-type SetFavoriteFolderParams struct {
236- FolderID sql.NullInt64 `json:"folder_id"`
237- ID int64 `json:"id"`
238-}
239-
240-func (q *Queries) SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error {
241- _, err := q.db.ExecContext(ctx, SetFavoriteFolder, arg.FolderID, arg.ID)
242- return err
243-}
244-
245191 const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec
246192 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
247193 `
@@ -11,8 +11,8 @@ import (
11 )11 )
12 12
13 const CreateFavorite = `-- name: CreateFavorite :one13 const CreateFavorite = `-- name: CreateFavorite :one
14-INSERT INTO favorites (url, title, favicon_url, folder_id, position, created_at, updated_at)14+INSERT INTO favorites (url, title, favicon_url, position, created_at, updated_at)
15-VALUES (?, ?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)15+VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorites), 0), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
16 RETURNING id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at16 RETURNING id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at
17 `17 `
18 18
@@ -20,16 +20,10 @@ type CreateFavoriteParams struct {
20 Url string `json:"url"`20 Url string `json:"url"`
21 Title sql.NullString `json:"title"`21 Title sql.NullString `json:"title"`
22 FaviconUrl sql.NullString `json:"favicon_url"`22 FaviconUrl sql.NullString `json:"favicon_url"`
23- FolderID sql.NullInt64 `json:"folder_id"`
24 }23 }
25 24
26 func (q *Queries) CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) {25 func (q *Queries) CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) {
27- row := q.db.QueryRowContext(ctx, CreateFavorite,26+ row := q.db.QueryRowContext(ctx, CreateFavorite, arg.Url, arg.Title, arg.FaviconUrl)
28- arg.Url,
29- arg.Title,
30- arg.FaviconUrl,
31- arg.FolderID,
32- )
33 var i Favorite27 var i Favorite
34 err := row.Scan(28 err := row.Scan(
35 &i.ID,29 &i.ID,
@@ -154,12 +148,15 @@ func (q *Queries) GetFavoriteByURL(ctx context.Context, url string) (Favorite, e
154 return i, err148 return i, err
155 }149 }
156 150
157-const GetFavoritesByFolder = `-- name: GetFavoritesByFolder :many151+const GetFavoritesByTag = `-- name: GetFavoritesByTag :many
158-SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id = ? ORDER BY position ASC152+SELECT f.id, f.url, f.title, f.favicon_url, f.folder_id, f.shortcut_key, f.position, f.created_at, f.updated_at FROM favorites f
153+INNER JOIN favorite_tag_assignments fta ON f.id = fta.favorite_id
154+WHERE fta.tag_id = ?
155+ORDER BY f.position ASC
159 `156 `
160 157
161-func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error) {158+func (q *Queries) GetFavoritesByTag(ctx context.Context, tagID int64) ([]Favorite, error) {
162- rows, err := q.db.QueryContext(ctx, GetFavoritesByFolder, folderID)159+ rows, err := q.db.QueryContext(ctx, GetFavoritesByTag, tagID)
163 if err != nil {160 if err != nil {
164 return nil, err161 return nil, err
165 }162 }
@@ -191,57 +188,6 @@ func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt
191 return items, nil188 return items, nil
192 }189 }
193 190
194-const GetFavoritesWithoutFolder = `-- name: GetFavoritesWithoutFolder :many
195-SELECT id, url, title, favicon_url, folder_id, shortcut_key, position, created_at, updated_at FROM favorites WHERE folder_id IS NULL ORDER BY position ASC
196-`
197-
198-func (q *Queries) GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) {
199- rows, err := q.db.QueryContext(ctx, GetFavoritesWithoutFolder)
200- if err != nil {
201- return nil, err
202- }
203- defer rows.Close()
204- items := []Favorite{}
205- for rows.Next() {
206- var i Favorite
207- if err := rows.Scan(
208- &i.ID,
209- &i.Url,
210- &i.Title,
211- &i.FaviconUrl,
212- &i.FolderID,
213- &i.ShortcutKey,
214- &i.Position,
215- &i.CreatedAt,
216- &i.UpdatedAt,
217- ); err != nil {
218- return nil, err
219- }
220- items = append(items, i)
221- }
222- if err := rows.Close(); err != nil {
223- return nil, err
224- }
225- if err := rows.Err(); err != nil {
226- return nil, err
227- }
228- return items, nil
229-}
230-
231-const SetFavoriteFolder = `-- name: SetFavoriteFolder :exec
232-UPDATE favorites SET folder_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
233-`
234-
235-type SetFavoriteFolderParams struct {
236- FolderID sql.NullInt64 `json:"folder_id"`
237- ID int64 `json:"id"`
238-}
239-
240-func (q *Queries) SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error {
241- _, err := q.db.ExecContext(ctx, SetFavoriteFolder, arg.FolderID, arg.ID)
242- return err
243-}
244-
245 const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec191 const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec
246 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?192 UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?
247 `193 `
D internal/infrastructure/persistence/sqlite/sqlc/folders.sql.go +0 -195
@@ -1,195 +0,0 @@
1-// Code generated by sqlc. DO NOT EDIT.
2-// versions:
3-// sqlc v1.30.0
4-// source: folders.sql
5-
6-package sqlc
7-
8-import (
9- "context"
10- "database/sql"
11-)
12-
13-const CreateFolder = `-- name: CreateFolder :one
14-INSERT INTO favorite_folders (name, icon, parent_id, position, created_at)
15-VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP)
16-RETURNING id, name, icon, parent_id, position, created_at
17-`
18-
19-type CreateFolderParams struct {
20- Name string `json:"name"`
21- Icon sql.NullString `json:"icon"`
22- ParentID sql.NullInt64 `json:"parent_id"`
23-}
24-
25-func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) {
26- row := q.db.QueryRowContext(ctx, CreateFolder, arg.Name, arg.Icon, arg.ParentID)
27- var i FavoriteFolder
28- err := row.Scan(
29- &i.ID,
30- &i.Name,
31- &i.Icon,
32- &i.ParentID,
33- &i.Position,
34- &i.CreatedAt,
35- )
36- return i, err
37-}
38-
39-const DeleteFolder = `-- name: DeleteFolder :exec
40-DELETE FROM favorite_folders WHERE id = ?
41-`
42-
43-func (q *Queries) DeleteFolder(ctx context.Context, id int64) error {
44- _, err := q.db.ExecContext(ctx, DeleteFolder, id)
45- return err
46-}
47-
48-const GetAllFolders = `-- name: GetAllFolders :many
49-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders ORDER BY position ASC
50-`
51-
52-func (q *Queries) GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) {
53- rows, err := q.db.QueryContext(ctx, GetAllFolders)
54- if err != nil {
55- return nil, err
56- }
57- defer rows.Close()
58- items := []FavoriteFolder{}
59- for rows.Next() {
60- var i FavoriteFolder
61- if err := rows.Scan(
62- &i.ID,
63- &i.Name,
64- &i.Icon,
65- &i.ParentID,
66- &i.Position,
67- &i.CreatedAt,
68- ); err != nil {
69- return nil, err
70- }
71- items = append(items, i)
72- }
73- if err := rows.Close(); err != nil {
74- return nil, err
75- }
76- if err := rows.Err(); err != nil {
77- return nil, err
78- }
79- return items, nil
80-}
81-
82-const GetChildFolders = `-- name: GetChildFolders :many
83-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC
84-`
85-
86-func (q *Queries) GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) {
87- rows, err := q.db.QueryContext(ctx, GetChildFolders, parentID)
88- if err != nil {
89- return nil, err
90- }
91- defer rows.Close()
92- items := []FavoriteFolder{}
93- for rows.Next() {
94- var i FavoriteFolder
95- if err := rows.Scan(
96- &i.ID,
97- &i.Name,
98- &i.Icon,
99- &i.ParentID,
100- &i.Position,
101- &i.CreatedAt,
102- ); err != nil {
103- return nil, err
104- }
105- items = append(items, i)
106- }
107- if err := rows.Close(); err != nil {
108- return nil, err
109- }
110- if err := rows.Err(); err != nil {
111- return nil, err
112- }
113- return items, nil
114-}
115-
116-const GetFolderByID = `-- name: GetFolderByID :one
117-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE id = ? LIMIT 1
118-`
119-
120-func (q *Queries) GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) {
121- row := q.db.QueryRowContext(ctx, GetFolderByID, id)
122- var i FavoriteFolder
123- err := row.Scan(
124- &i.ID,
125- &i.Name,
126- &i.Icon,
127- &i.ParentID,
128- &i.Position,
129- &i.CreatedAt,
130- )
131- return i, err
132-}
133-
134-const GetRootFolders = `-- name: GetRootFolders :many
135-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC
136-`
137-
138-func (q *Queries) GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) {
139- rows, err := q.db.QueryContext(ctx, GetRootFolders)
140- if err != nil {
141- return nil, err
142- }
143- defer rows.Close()
144- items := []FavoriteFolder{}
145- for rows.Next() {
146- var i FavoriteFolder
147- if err := rows.Scan(
148- &i.ID,
149- &i.Name,
150- &i.Icon,
151- &i.ParentID,
152- &i.Position,
153- &i.CreatedAt,
154- ); err != nil {
155- return nil, err
156- }
157- items = append(items, i)
158- }
159- if err := rows.Close(); err != nil {
160- return nil, err
161- }
162- if err := rows.Err(); err != nil {
163- return nil, err
164- }
165- return items, nil
166-}
167-
168-const UpdateFolder = `-- name: UpdateFolder :exec
169-UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?
170-`
171-
172-type UpdateFolderParams struct {
173- Name string `json:"name"`
174- Icon sql.NullString `json:"icon"`
175- ID int64 `json:"id"`
176-}
177-
178-func (q *Queries) UpdateFolder(ctx context.Context, arg UpdateFolderParams) error {
179- _, err := q.db.ExecContext(ctx, UpdateFolder, arg.Name, arg.Icon, arg.ID)
180- return err
181-}
182-
183-const UpdateFolderPosition = `-- name: UpdateFolderPosition :exec
184-UPDATE favorite_folders SET position = ? WHERE id = ?
185-`
186-
187-type UpdateFolderPositionParams struct {
188- Position int64 `json:"position"`
189- ID int64 `json:"id"`
190-}
191-
192-func (q *Queries) UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error {
193- _, err := q.db.ExecContext(ctx, UpdateFolderPosition, arg.Position, arg.ID)
194- return err
195-}
@@ -1,195 +0,0 @@
1-// Code generated by sqlc. DO NOT EDIT.
2-// versions:
3-// sqlc v1.30.0
4-// source: folders.sql
5-
6-package sqlc
7-
8-import (
9- "context"
10- "database/sql"
11-)
12-
13-const CreateFolder = `-- name: CreateFolder :one
14-INSERT INTO favorite_folders (name, icon, parent_id, position, created_at)
15-VALUES (?, ?, ?, COALESCE((SELECT MAX(position) + 1 FROM favorite_folders), 0), CURRENT_TIMESTAMP)
16-RETURNING id, name, icon, parent_id, position, created_at
17-`
18-
19-type CreateFolderParams struct {
20- Name string `json:"name"`
21- Icon sql.NullString `json:"icon"`
22- ParentID sql.NullInt64 `json:"parent_id"`
23-}
24-
25-func (q *Queries) CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) {
26- row := q.db.QueryRowContext(ctx, CreateFolder, arg.Name, arg.Icon, arg.ParentID)
27- var i FavoriteFolder
28- err := row.Scan(
29- &i.ID,
30- &i.Name,
31- &i.Icon,
32- &i.ParentID,
33- &i.Position,
34- &i.CreatedAt,
35- )
36- return i, err
37-}
38-
39-const DeleteFolder = `-- name: DeleteFolder :exec
40-DELETE FROM favorite_folders WHERE id = ?
41-`
42-
43-func (q *Queries) DeleteFolder(ctx context.Context, id int64) error {
44- _, err := q.db.ExecContext(ctx, DeleteFolder, id)
45- return err
46-}
47-
48-const GetAllFolders = `-- name: GetAllFolders :many
49-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders ORDER BY position ASC
50-`
51-
52-func (q *Queries) GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) {
53- rows, err := q.db.QueryContext(ctx, GetAllFolders)
54- if err != nil {
55- return nil, err
56- }
57- defer rows.Close()
58- items := []FavoriteFolder{}
59- for rows.Next() {
60- var i FavoriteFolder
61- if err := rows.Scan(
62- &i.ID,
63- &i.Name,
64- &i.Icon,
65- &i.ParentID,
66- &i.Position,
67- &i.CreatedAt,
68- ); err != nil {
69- return nil, err
70- }
71- items = append(items, i)
72- }
73- if err := rows.Close(); err != nil {
74- return nil, err
75- }
76- if err := rows.Err(); err != nil {
77- return nil, err
78- }
79- return items, nil
80-}
81-
82-const GetChildFolders = `-- name: GetChildFolders :many
83-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id = ? ORDER BY position ASC
84-`
85-
86-func (q *Queries) GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) {
87- rows, err := q.db.QueryContext(ctx, GetChildFolders, parentID)
88- if err != nil {
89- return nil, err
90- }
91- defer rows.Close()
92- items := []FavoriteFolder{}
93- for rows.Next() {
94- var i FavoriteFolder
95- if err := rows.Scan(
96- &i.ID,
97- &i.Name,
98- &i.Icon,
99- &i.ParentID,
100- &i.Position,
101- &i.CreatedAt,
102- ); err != nil {
103- return nil, err
104- }
105- items = append(items, i)
106- }
107- if err := rows.Close(); err != nil {
108- return nil, err
109- }
110- if err := rows.Err(); err != nil {
111- return nil, err
112- }
113- return items, nil
114-}
115-
116-const GetFolderByID = `-- name: GetFolderByID :one
117-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE id = ? LIMIT 1
118-`
119-
120-func (q *Queries) GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) {
121- row := q.db.QueryRowContext(ctx, GetFolderByID, id)
122- var i FavoriteFolder
123- err := row.Scan(
124- &i.ID,
125- &i.Name,
126- &i.Icon,
127- &i.ParentID,
128- &i.Position,
129- &i.CreatedAt,
130- )
131- return i, err
132-}
133-
134-const GetRootFolders = `-- name: GetRootFolders :many
135-SELECT id, name, icon, parent_id, position, created_at FROM favorite_folders WHERE parent_id IS NULL ORDER BY position ASC
136-`
137-
138-func (q *Queries) GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) {
139- rows, err := q.db.QueryContext(ctx, GetRootFolders)
140- if err != nil {
141- return nil, err
142- }
143- defer rows.Close()
144- items := []FavoriteFolder{}
145- for rows.Next() {
146- var i FavoriteFolder
147- if err := rows.Scan(
148- &i.ID,
149- &i.Name,
150- &i.Icon,
151- &i.ParentID,
152- &i.Position,
153- &i.CreatedAt,
154- ); err != nil {
155- return nil, err
156- }
157- items = append(items, i)
158- }
159- if err := rows.Close(); err != nil {
160- return nil, err
161- }
162- if err := rows.Err(); err != nil {
163- return nil, err
164- }
165- return items, nil
166-}
167-
168-const UpdateFolder = `-- name: UpdateFolder :exec
169-UPDATE favorite_folders SET name = ?, icon = ? WHERE id = ?
170-`
171-
172-type UpdateFolderParams struct {
173- Name string `json:"name"`
174- Icon sql.NullString `json:"icon"`
175- ID int64 `json:"id"`
176-}
177-
178-func (q *Queries) UpdateFolder(ctx context.Context, arg UpdateFolderParams) error {
179- _, err := q.db.ExecContext(ctx, UpdateFolder, arg.Name, arg.Icon, arg.ID)
180- return err
181-}
182-
183-const UpdateFolderPosition = `-- name: UpdateFolderPosition :exec
184-UPDATE favorite_folders SET position = ? WHERE id = ?
185-`
186-
187-type UpdateFolderPositionParams struct {
188- Position int64 `json:"position"`
189- ID int64 `json:"id"`
190-}
191-
192-func (q *Queries) UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error {
193- _, err := q.db.ExecContext(ctx, UpdateFolderPosition, arg.Position, arg.ID)
194- return err
195-}
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +3 -12
@@ -14,14 +14,12 @@ type Querier interface {
1414 AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error
1515 CapVisitCount(ctx context.Context, arg CapVisitCountParams) error
1616 CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error)
17- CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error)
1817 CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error)
1918 DeleteAllHistory(ctx context.Context) error
2019 // Deletes exited browser sessions older than the given cutoff time.
2120 DeleteExitedSessionsBefore(ctx context.Context, endedAt sql.NullTime) (int64, error)
2221 DeleteFavicon(ctx context.Context, key string) error
2322 DeleteFavorite(ctx context.Context, id int64) error
24- DeleteFolder(ctx context.Context, id int64) error
2523 DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error
2624 DeleteHistoryByID(ctx context.Context, id int64) error
2725 DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error
@@ -35,23 +33,19 @@ type Querier interface {
3533 DeleteZoomLevel(ctx context.Context, domain string) error
3634 GetActiveBrowserSession(ctx context.Context) (Session, error)
3735 GetAllFavorites(ctx context.Context) ([]Favorite, error)
38- GetAllFolders(ctx context.Context) ([]FavoriteFolder, error)
3936 GetAllMostVisited(ctx context.Context) ([]History, error)
4037 GetAllRecentHistory(ctx context.Context) ([]History, error)
4138 GetAllRecentHistoryByDomain(ctx context.Context, domain sql.NullString) ([]History, error)
4239 GetAllSessionStates(ctx context.Context) ([]SessionState, error)
4340 GetAllTags(ctx context.Context) ([]FavoriteTag, error)
4441 GetAllWhitelistedDomains(ctx context.Context) ([]string, error)
45- GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error)
4642 GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error)
4743 GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error)
4844 GetFavicon(ctx context.Context, key string) (Favicon, error)
4945 GetFavoriteByID(ctx context.Context, id int64) (Favorite, error)
5046 GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error)
5147 GetFavoriteByURL(ctx context.Context, url string) (Favorite, error)
52- GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error)
53- GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error)
54- GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error)
48+ GetFavoritesByTag(ctx context.Context, tagID int64) ([]Favorite, error)
5549 GetHistoryByURL(ctx context.Context, url string) (History, error)
5650 GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error)
5751 GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error)
@@ -63,13 +57,13 @@ type Querier interface {
6357 GetRecentHistoryWindow(ctx context.Context, arg GetRecentHistoryWindowParams) ([]History, error)
6458 GetRecentHistoryWindowByDomain(ctx context.Context, arg GetRecentHistoryWindowByDomainParams) ([]History, error)
6559 GetRecentSessions(ctx context.Context, limit int64) ([]Session, error)
66- GetRootFolders(ctx context.Context) ([]FavoriteFolder, error)
6760 GetSessionByID(ctx context.Context, id string) (Session, error)
6861 GetSessionState(ctx context.Context, sessionID string) (SessionState, error)
6962 GetSessionsWithState(ctx context.Context, limit int64) ([]GetSessionsWithStateRow, error)
7063 GetTagByID(ctx context.Context, id int64) (FavoriteTag, error)
71- GetTagByName(ctx context.Context, name string) (FavoriteTag, error)
64+ GetTagByName(ctx context.Context, trim string) (FavoriteTag, error)
7265 GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error)
66+ GetTagsForFavorites(ctx context.Context, favoriteIds []int64) ([]GetTagsForFavoritesRow, error)
7367 GetTotalSessionStatesSize(ctx context.Context) (interface{}, error)
7468 GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error)
7569 IncrementVisitCount(ctx context.Context, url string) error
@@ -86,15 +80,12 @@ type Querier interface {
8680 SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error)
8781 SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error)
8882 SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error)
89- SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error
9083 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error
9184 SetPermission(ctx context.Context, arg SetPermissionParams) error
9285 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error
9386 UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error
9487 UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error
9588 UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error
96- UpdateFolder(ctx context.Context, arg UpdateFolderParams) error
97- UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error
9889 UpdateTag(ctx context.Context, arg UpdateTagParams) error
9990 UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error
10091 UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error
@@ -14,14 +14,12 @@ type Querier interface {
14 AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error14 AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error
15 CapVisitCount(ctx context.Context, arg CapVisitCountParams) error15 CapVisitCount(ctx context.Context, arg CapVisitCountParams) error
16 CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error)16 CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error)
17- CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error)
18 CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error)17 CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error)
19 DeleteAllHistory(ctx context.Context) error18 DeleteAllHistory(ctx context.Context) error
20 // Deletes exited browser sessions older than the given cutoff time.19 // Deletes exited browser sessions older than the given cutoff time.
21 DeleteExitedSessionsBefore(ctx context.Context, endedAt sql.NullTime) (int64, error)20 DeleteExitedSessionsBefore(ctx context.Context, endedAt sql.NullTime) (int64, error)
22 DeleteFavicon(ctx context.Context, key string) error21 DeleteFavicon(ctx context.Context, key string) error
23 DeleteFavorite(ctx context.Context, id int64) error22 DeleteFavorite(ctx context.Context, id int64) error
24- DeleteFolder(ctx context.Context, id int64) error
25 DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error23 DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error
26 DeleteHistoryByID(ctx context.Context, id int64) error24 DeleteHistoryByID(ctx context.Context, id int64) error
27 DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error25 DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error
@@ -35,23 +33,19 @@ type Querier interface {
35 DeleteZoomLevel(ctx context.Context, domain string) error33 DeleteZoomLevel(ctx context.Context, domain string) error
36 GetActiveBrowserSession(ctx context.Context) (Session, error)34 GetActiveBrowserSession(ctx context.Context) (Session, error)
37 GetAllFavorites(ctx context.Context) ([]Favorite, error)35 GetAllFavorites(ctx context.Context) ([]Favorite, error)
38- GetAllFolders(ctx context.Context) ([]FavoriteFolder, error)
39 GetAllMostVisited(ctx context.Context) ([]History, error)36 GetAllMostVisited(ctx context.Context) ([]History, error)
40 GetAllRecentHistory(ctx context.Context) ([]History, error)37 GetAllRecentHistory(ctx context.Context) ([]History, error)
41 GetAllRecentHistoryByDomain(ctx context.Context, domain sql.NullString) ([]History, error)38 GetAllRecentHistoryByDomain(ctx context.Context, domain sql.NullString) ([]History, error)
42 GetAllSessionStates(ctx context.Context) ([]SessionState, error)39 GetAllSessionStates(ctx context.Context) ([]SessionState, error)
43 GetAllTags(ctx context.Context) ([]FavoriteTag, error)40 GetAllTags(ctx context.Context) ([]FavoriteTag, error)
44 GetAllWhitelistedDomains(ctx context.Context) ([]string, error)41 GetAllWhitelistedDomains(ctx context.Context) ([]string, error)
45- GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error)
46 GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error)42 GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error)
47 GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error)43 GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error)
48 GetFavicon(ctx context.Context, key string) (Favicon, error)44 GetFavicon(ctx context.Context, key string) (Favicon, error)
49 GetFavoriteByID(ctx context.Context, id int64) (Favorite, error)45 GetFavoriteByID(ctx context.Context, id int64) (Favorite, error)
50 GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error)46 GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error)
51 GetFavoriteByURL(ctx context.Context, url string) (Favorite, error)47 GetFavoriteByURL(ctx context.Context, url string) (Favorite, error)
52- GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt64) ([]Favorite, error)48+ GetFavoritesByTag(ctx context.Context, tagID int64) ([]Favorite, error)
53- GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error)
54- GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error)
55 GetHistoryByURL(ctx context.Context, url string) (History, error)49 GetHistoryByURL(ctx context.Context, url string) (History, error)
56 GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error)50 GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error)
57 GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error)51 GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error)
@@ -63,13 +57,13 @@ type Querier interface {
63 GetRecentHistoryWindow(ctx context.Context, arg GetRecentHistoryWindowParams) ([]History, error)57 GetRecentHistoryWindow(ctx context.Context, arg GetRecentHistoryWindowParams) ([]History, error)
64 GetRecentHistoryWindowByDomain(ctx context.Context, arg GetRecentHistoryWindowByDomainParams) ([]History, error)58 GetRecentHistoryWindowByDomain(ctx context.Context, arg GetRecentHistoryWindowByDomainParams) ([]History, error)
65 GetRecentSessions(ctx context.Context, limit int64) ([]Session, error)59 GetRecentSessions(ctx context.Context, limit int64) ([]Session, error)
66- GetRootFolders(ctx context.Context) ([]FavoriteFolder, error)
67 GetSessionByID(ctx context.Context, id string) (Session, error)60 GetSessionByID(ctx context.Context, id string) (Session, error)
68 GetSessionState(ctx context.Context, sessionID string) (SessionState, error)61 GetSessionState(ctx context.Context, sessionID string) (SessionState, error)
69 GetSessionsWithState(ctx context.Context, limit int64) ([]GetSessionsWithStateRow, error)62 GetSessionsWithState(ctx context.Context, limit int64) ([]GetSessionsWithStateRow, error)
70 GetTagByID(ctx context.Context, id int64) (FavoriteTag, error)63 GetTagByID(ctx context.Context, id int64) (FavoriteTag, error)
71- GetTagByName(ctx context.Context, name string) (FavoriteTag, error)64+ GetTagByName(ctx context.Context, trim string) (FavoriteTag, error)
72 GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error)65 GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error)
66+ GetTagsForFavorites(ctx context.Context, favoriteIds []int64) ([]GetTagsForFavoritesRow, error)
73 GetTotalSessionStatesSize(ctx context.Context) (interface{}, error)67 GetTotalSessionStatesSize(ctx context.Context) (interface{}, error)
74 GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error)68 GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error)
75 IncrementVisitCount(ctx context.Context, url string) error69 IncrementVisitCount(ctx context.Context, url string) error
@@ -86,15 +80,12 @@ type Querier interface {
86 SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error)80 SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error)
87 SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error)81 SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error)
88 SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error)82 SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error)
89- SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error
90 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error83 SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error
91 SetPermission(ctx context.Context, arg SetPermissionParams) error84 SetPermission(ctx context.Context, arg SetPermissionParams) error
92 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error85 SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error
93 UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error86 UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error
94 UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error87 UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error
95 UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error88 UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error
96- UpdateFolder(ctx context.Context, arg UpdateFolderParams) error
97- UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error
98 UpdateTag(ctx context.Context, arg UpdateTagParams) error89 UpdateTag(ctx context.Context, arg UpdateTagParams) error
99 UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error90 UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error
100 UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error91 UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error
M internal/infrastructure/persistence/sqlite/sqlc/tags.sql.go +55 -3
@@ -7,6 +7,7 @@ package sqlc
77
88 import (
99 "context"
10+ "strings"
1011 )
1112
1213 const AssignTagToFavorite = `-- name: AssignTagToFavorite :exec
@@ -102,11 +103,11 @@ func (q *Queries) GetTagByID(ctx context.Context, id int64) (FavoriteTag, error)
102103 }
103104
104105 const GetTagByName = `-- name: GetTagByName :one
105-SELECT id, name, color, created_at FROM favorite_tags WHERE name = ? LIMIT 1
106+SELECT id, name, color, created_at FROM favorite_tags WHERE lower(trim(name)) = lower(trim(?)) LIMIT 1
106107 `
107108
108-func (q *Queries) GetTagByName(ctx context.Context, name string) (FavoriteTag, error) {
109- row := q.db.QueryRowContext(ctx, GetTagByName, name)
109+func (q *Queries) GetTagByName(ctx context.Context, trim string) (FavoriteTag, error) {
110+ row := q.db.QueryRowContext(ctx, GetTagByName, trim)
110111 var i FavoriteTag
111112 err := row.Scan(
112113 &i.ID,
@@ -152,6 +153,57 @@ func (q *Queries) GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]F
152153 return items, nil
153154 }
154155
156+const GetTagsForFavorites = `-- name: GetTagsForFavorites :many
157+SELECT t.id, t.name, t.color, t.created_at, fta.favorite_id FROM favorite_tags t
158+INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
159+WHERE fta.favorite_id IN (/*SLICE:favorite_ids*/?)
160+ORDER BY fta.favorite_id ASC, t.name ASC
161+`
162+
163+type GetTagsForFavoritesRow struct {
164+ FavoriteTag FavoriteTag `json:"favorite_tag"`
165+ FavoriteID int64 `json:"favorite_id"`
166+}
167+
168+func (q *Queries) GetTagsForFavorites(ctx context.Context, favoriteIds []int64) ([]GetTagsForFavoritesRow, error) {
169+ query := GetTagsForFavorites
170+ var queryParams []interface{}
171+ if len(favoriteIds) > 0 {
172+ for _, v := range favoriteIds {
173+ queryParams = append(queryParams, v)
174+ }
175+ query = strings.Replace(query, "/*SLICE:favorite_ids*/?", strings.Repeat(",?", len(favoriteIds))[1:], 1)
176+ } else {
177+ query = strings.Replace(query, "/*SLICE:favorite_ids*/?", "NULL", 1)
178+ }
179+ rows, err := q.db.QueryContext(ctx, query, queryParams...)
180+ if err != nil {
181+ return nil, err
182+ }
183+ defer rows.Close()
184+ items := []GetTagsForFavoritesRow{}
185+ for rows.Next() {
186+ var i GetTagsForFavoritesRow
187+ if err := rows.Scan(
188+ &i.FavoriteTag.ID,
189+ &i.FavoriteTag.Name,
190+ &i.FavoriteTag.Color,
191+ &i.FavoriteTag.CreatedAt,
192+ &i.FavoriteID,
193+ ); err != nil {
194+ return nil, err
195+ }
196+ items = append(items, i)
197+ }
198+ if err := rows.Close(); err != nil {
199+ return nil, err
200+ }
201+ if err := rows.Err(); err != nil {
202+ return nil, err
203+ }
204+ return items, nil
205+}
206+
155207 const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec
156208 DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ?
157209 `
@@ -7,6 +7,7 @@ package sqlc
7 7
8 import (8 import (
9 "context"9 "context"
10+ "strings"
10 )11 )
11 12
12 const AssignTagToFavorite = `-- name: AssignTagToFavorite :exec13 const AssignTagToFavorite = `-- name: AssignTagToFavorite :exec
@@ -102,11 +103,11 @@ func (q *Queries) GetTagByID(ctx context.Context, id int64) (FavoriteTag, error)
102 }103 }
103 104
104 const GetTagByName = `-- name: GetTagByName :one105 const GetTagByName = `-- name: GetTagByName :one
105-SELECT id, name, color, created_at FROM favorite_tags WHERE name = ? LIMIT 1106+SELECT id, name, color, created_at FROM favorite_tags WHERE lower(trim(name)) = lower(trim(?)) LIMIT 1
106 `107 `
107 108
108-func (q *Queries) GetTagByName(ctx context.Context, name string) (FavoriteTag, error) {109+func (q *Queries) GetTagByName(ctx context.Context, trim string) (FavoriteTag, error) {
109- row := q.db.QueryRowContext(ctx, GetTagByName, name)110+ row := q.db.QueryRowContext(ctx, GetTagByName, trim)
110 var i FavoriteTag111 var i FavoriteTag
111 err := row.Scan(112 err := row.Scan(
112 &i.ID,113 &i.ID,
@@ -152,6 +153,57 @@ func (q *Queries) GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]F
152 return items, nil153 return items, nil
153 }154 }
154 155
156+const GetTagsForFavorites = `-- name: GetTagsForFavorites :many
157+SELECT t.id, t.name, t.color, t.created_at, fta.favorite_id FROM favorite_tags t
158+INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id
159+WHERE fta.favorite_id IN (/*SLICE:favorite_ids*/?)
160+ORDER BY fta.favorite_id ASC, t.name ASC
161+`
162+
163+type GetTagsForFavoritesRow struct {
164+ FavoriteTag FavoriteTag `json:"favorite_tag"`
165+ FavoriteID int64 `json:"favorite_id"`
166+}
167+
168+func (q *Queries) GetTagsForFavorites(ctx context.Context, favoriteIds []int64) ([]GetTagsForFavoritesRow, error) {
169+ query := GetTagsForFavorites
170+ var queryParams []interface{}
171+ if len(favoriteIds) > 0 {
172+ for _, v := range favoriteIds {
173+ queryParams = append(queryParams, v)
174+ }
175+ query = strings.Replace(query, "/*SLICE:favorite_ids*/?", strings.Repeat(",?", len(favoriteIds))[1:], 1)
176+ } else {
177+ query = strings.Replace(query, "/*SLICE:favorite_ids*/?", "NULL", 1)
178+ }
179+ rows, err := q.db.QueryContext(ctx, query, queryParams...)
180+ if err != nil {
181+ return nil, err
182+ }
183+ defer rows.Close()
184+ items := []GetTagsForFavoritesRow{}
185+ for rows.Next() {
186+ var i GetTagsForFavoritesRow
187+ if err := rows.Scan(
188+ &i.FavoriteTag.ID,
189+ &i.FavoriteTag.Name,
190+ &i.FavoriteTag.Color,
191+ &i.FavoriteTag.CreatedAt,
192+ &i.FavoriteID,
193+ ); err != nil {
194+ return nil, err
195+ }
196+ items = append(items, i)
197+ }
198+ if err := rows.Close(); err != nil {
199+ return nil, err
200+ }
201+ if err := rows.Err(); err != nil {
202+ return nil, err
203+ }
204+ return items, nil
205+}
206+
155 const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec207 const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec
156 DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ?208 DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ?
157 `209 `

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close