refactor(favorites): migrate folders to tags
- commit
- ecab2f7f6c85d702ff41106bc7366dfa8695d9c2
- author
- brice · · 2026-06-28 07:17 UTC
- parent
- 2ae1fc7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 14 | 14 | // ==================== Favorite Repository ==================== | |
| 15 | 15 | ||
| 16 | 16 | type favoriteRepo struct { | |
| 17 | - | db *sql.DB | |
| 18 | 17 | queries *sqlc.Queries | |
| 19 | 18 | } | |
| 20 | 19 | ||
| 21 | 20 | // NewFavoriteRepository creates a new SQLite-backed favorite repository. | |
| 22 | 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 | 25 | func (r *favoriteRepo) Save(ctx context.Context, fav *entity.Favorite) error { | |
| 27 | 26 | log := logging.FromContext(ctx) | |
| 28 | 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 | 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 | 37 | row, err := r.queries.CreateFavorite(ctx, sqlc.CreateFavoriteParams{ | |
| 40 | 38 | Url: fav.URL, | |
| 41 | 39 | Title: sql.NullString{String: fav.Title, Valid: fav.Title != ""}, | |
| 42 | 40 | FaviconUrl: sql.NullString{String: fav.FaviconURL, Valid: fav.FaviconURL != ""}, | |
| 43 | - | FolderID: folderID, | |
| 44 | 41 | }) | |
| 45 | 42 | if err != nil { | |
| 46 | 43 | return err | |
| 47 | 44 | } | |
| 48 | - | fav.ID = entity.FavoriteID(row.ID) | |
| 45 | + | favoriteFromRow(row, fav) | |
| 49 | 46 | 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 | 49 | func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*entity.Favorite, error) { | |
| 90 | 50 | row, err := r.queries.GetFavoriteByID(ctx, int64(id)) | |
| 91 | 51 | if err != nil { | |
| @@ -94,7 +54,8 @@ func (r *favoriteRepo) FindByID(ctx context.Context, id entity.FavoriteID) (*ent | |||
| 94 | 54 | } | |
| 95 | 55 | return nil, err | |
| 96 | 56 | } | |
| 97 | - | return favoriteFromRow(row), nil | |
| 57 | + | fav := favoriteFromRow(row, nil) | |
| 58 | + | return fav, r.hydrateTags(ctx, []*entity.Favorite{fav}) | |
| 98 | 59 | } | |
| 99 | 60 | ||
| 100 | 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 | 67 | return nil, err | |
| 107 | 68 | } | |
| 108 | - | return favoriteFromRow(row), nil | |
| 69 | + | fav := favoriteFromRow(row, nil) | |
| 70 | + | return fav, r.hydrateTags(ctx, []*entity.Favorite{fav}) | |
| 109 | 71 | } | |
| 110 | 72 | ||
| 111 | 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 | 75 | if err != nil { | |
| 114 | 76 | return nil, err | |
| 115 | 77 | } | |
| 116 | - | return favoritesFromRows(rows), nil | |
| 78 | + | 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) { | |
| 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)) | |
| 128 | 84 | if err != nil { | |
| 129 | 85 | return nil, err | |
| 130 | 86 | } | |
| 131 | - | return favoritesFromRows(rows), nil | |
| 87 | + | favorites := favoritesFromRows(rows) | |
| 88 | + | return favorites, r.hydrateTags(ctx, favorites) | |
| 132 | 89 | } | |
| 133 | 90 | ||
| 134 | 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 | 97 | return nil, err | |
| 141 | 98 | } | |
| 142 | - | return favoriteFromRow(row), nil | |
| 99 | + | fav := favoriteFromRow(row, nil) | |
| 100 | + | return fav, r.hydrateTags(ctx, []*entity.Favorite{fav}) | |
| 143 | 101 | } | |
| 144 | 102 | ||
| 145 | 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 | 110 | func (r *favoriteRepo) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error { | |
| 164 | 111 | var k sql.NullInt64 | |
| 165 | 112 | if key != nil { | |
| @@ -175,141 +122,67 @@ func (r *favoriteRepo) Delete(ctx context.Context, id entity.FavoriteID) error { | |||
| 175 | 122 | return r.queries.DeleteFavorite(ctx, int64(id)) | |
| 176 | 123 | } | |
| 177 | 124 | ||
| 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 | |
| 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. | |
| 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 | |
| 225 | 139 | } | |
| 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 | |
| 233 | 142 | } | |
| 234 | 143 | ||
| 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) | |
| 240 | 145 | if err != nil { | |
| 241 | 146 | return err | |
| 242 | 147 | } | |
| 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 | |
| 252 | 152 | } | |
| 253 | - | return nil, err | |
| 153 | + | fav.Tags = append(fav.Tags, *tagFromRow(row.FavoriteTag)) | |
| 254 | 154 | } | |
| 255 | - | return folderFromRow(row), nil | |
| 155 | + | return nil | |
| 256 | 156 | } | |
| 257 | 157 | ||
| 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{} | |
| 262 | 162 | } | |
| 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 | |
| 272 | 174 | } 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 | |
| 277 | 176 | } | |
| 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 | |
| 305 | 178 | } | |
| 306 | 179 | ||
| 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) | |
| 311 | 184 | } | |
| 312 | - | return folders | |
| 185 | + | return favorites | |
| 313 | 186 | } | |
| 314 | 187 | ||
| 315 | 188 | // ==================== 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.Queries | 17 | 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 err | 43 | return err | ||
| 47 | } | 44 | } | ||
| 48 | - | fav.ID = entity.FavoriteID(row.ID) | 45 | + | favoriteFromRow(row, fav) |
| 49 | return nil | 46 | 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, err | 55 | return nil, err | ||
| 96 | } | 56 | } | ||
| 97 | - | return favoriteFromRow(row), nil | 57 | + | 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, err | 67 | return nil, err | ||
| 107 | } | 68 | } | ||
| 108 | - | return favoriteFromRow(row), nil | 69 | + | 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, err | 76 | return nil, err | ||
| 115 | } | 77 | } | ||
| 116 | - | return favoritesFromRows(rows), nil | 78 | + | 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.Favorite | 83 | + | 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, err | 85 | return nil, err | ||
| 130 | } | 86 | } | ||
| 131 | - | return favoritesFromRows(rows), nil | 87 | + | 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, err | 97 | return nil, err | ||
| 141 | } | 98 | } | ||
| 142 | - | return favoriteFromRow(row), nil | 99 | + | 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.NullInt64 | 111 | 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 err | 146 | return err | ||
| 242 | } | 147 | } | ||
| 243 | - | folder.ID = entity.FolderID(row.ID) | 148 | + | for _, row := range rows { |
| 244 | - | return nil | 149 | + | 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, err | 153 | + | fav.Tags = append(fav.Tags, *tagFromRow(row.FavoriteTag)) |
| 254 | } | 154 | } | ||
| 255 | - | return folderFromRow(row), nil | 155 | + | 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, err | 161 | + | fav = &entity.Favorite{} |
| 262 | } | 162 | } | ||
| 263 | - | return foldersFromRows(rows), nil | 163 | + | 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.FavoriteFolder | 167 | + | fav.Position = int(row.Position) |
| 268 | - | var err error | 168 | + | 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), nil | 177 | + | 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 folders | 185 | + | 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 | |||
| 357 | 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 | 361 | if err := r.init(ctx); err != nil { | |
| 362 | 362 | return nil, err | |
| 363 | 363 | } | |
| 364 | - | return r.repo.GetByFolder(ctx, folderID) | |
| 364 | + | return r.repo.GetByTag(ctx, tagID) | |
| 365 | 365 | } | |
| 366 | 366 | ||
| 367 | 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 | 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 | 381 | func (r *LazyFavoriteRepository) SetShortcut(ctx context.Context, id entity.FavoriteID, key *int) error { | |
| 389 | 382 | if err := r.init(ctx); err != nil { | |
| 390 | 383 | return err | |
| @@ -399,73 +392,6 @@ func (r *LazyFavoriteRepository) Delete(ctx context.Context, id entity.FavoriteI | |||
| 399 | 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 | 395 | // LazyTagRepository wraps a tag repository with lazy database initialization. | |
| 470 | 396 | type LazyTagRepository struct { | |
| 471 | 397 | provider port.DatabaseProvider | |
| @@ -798,7 +724,6 @@ func (r *LazyContentWhitelistRepository) GetAll(ctx context.Context) ([]string, | |||
| 798 | 724 | type LazyRepositories struct { | |
| 799 | 725 | History repository.HistoryRepository | |
| 800 | 726 | Favorite repository.FavoriteRepository | |
| 801 | - | Folder repository.FolderRepository | |
| 802 | 727 | Tag repository.TagRepository | |
| 803 | 728 | Zoom repository.ZoomRepository | |
| 804 | 729 | Session repository.SessionRepository | |
| @@ -812,7 +737,6 @@ func NewLazyRepositories(provider port.DatabaseProvider) *LazyRepositories { | |||
| 812 | 737 | return &LazyRepositories{ | |
| 813 | 738 | History: NewLazyHistoryRepository(provider), | |
| 814 | 739 | Favorite: NewLazyFavoriteRepository(provider), | |
| 815 | - | Folder: NewLazyFolderRepository(provider), | |
| 816 | 740 | Tag: NewLazyTagRepository(provider), | |
| 817 | 741 | Zoom: NewLazyZoomRepository(provider), | |
| 818 | 742 | 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, err | 362 | 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 err | 383 | 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.DatabaseProvider | 397 | 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.HistoryRepository | 725 | History repository.HistoryRepository | ||
| 800 | Favorite repository.FavoriteRepository | 726 | Favorite repository.FavoriteRepository | ||
| 801 | - | Folder repository.FolderRepository | |||
| 802 | Tag repository.TagRepository | 727 | Tag repository.TagRepository | ||
| 803 | Zoom repository.ZoomRepository | 728 | Zoom repository.ZoomRepository | ||
| 804 | Session repository.SessionRepository | 729 | 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; | |||
| 7 | 7 | -- name: GetFavoriteByURL :one | |
| 8 | 8 | SELECT * FROM favorites WHERE url = ? LIMIT 1; | |
| 9 | 9 | ||
| 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; | |
| 15 | 15 | ||
| 16 | 16 | -- name: GetFavoriteByShortcut :one | |
| 17 | 17 | SELECT * FROM favorites WHERE shortcut_key = ? LIMIT 1; | |
| 18 | 18 | ||
| 19 | 19 | -- 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) | |
| 22 | 22 | RETURNING *; | |
| 23 | 23 | ||
| 24 | 24 | -- name: UpdateFavorite :exec | |
| @@ -29,9 +29,6 @@ WHERE id = ?; | |||
| 29 | 29 | -- name: UpdateFavoritePosition :exec | |
| 30 | 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 | 32 | -- name: SetFavoriteShortcut :exec | |
| 36 | 33 | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?; | |
| 37 | 34 | ||
| @@ -7,18 +7,18 @@ SELECT * FROM favorites WHERE id = ? LIMIT 1; | |||||
| 7 | -- name: GetFavoriteByURL :one | 7 | -- name: GetFavoriteByURL :one | ||
| 8 | SELECT * FROM favorites WHERE url = ? LIMIT 1; | 8 | SELECT * FROM favorites WHERE url = ? LIMIT 1; | ||
| 9 | 9 | ||||
| 10 | - | -- name: GetFavoritesByFolder :many | 10 | + | -- 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 :many | 13 | + | 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 :one | 16 | -- 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 :one | 19 | -- 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 :exec | 24 | -- name: UpdateFavorite :exec | ||
| @@ -29,9 +29,6 @@ WHERE id = ?; | |||||
| 29 | -- name: UpdateFavoritePosition :exec | 29 | -- 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 :exec | 32 | -- 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; | |||
| 5 | 5 | SELECT * FROM favorite_tags WHERE id = ? LIMIT 1; | |
| 6 | 6 | ||
| 7 | 7 | -- 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 | 10 | -- name: CreateTag :one | |
| 11 | 11 | INSERT INTO favorite_tags (name, color) VALUES (?, ?) RETURNING *; | |
| @@ -27,3 +27,9 @@ SELECT t.* FROM favorite_tags t | |||
| 27 | 27 | INNER JOIN favorite_tag_assignments fta ON t.id = fta.tag_id | |
| 28 | 28 | WHERE fta.favorite_id = ? | |
| 29 | 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; | |
| @@ -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 :one | 7 | -- 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 :one | 10 | -- 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_id | 27 | 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 ( | |||
| 11 | 11 | ) | |
| 12 | 12 | ||
| 13 | 13 | 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) | |
| 16 | 16 | 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 | 20 | Url string `json:"url"` | |
| 21 | 21 | Title sql.NullString `json:"title"` | |
| 22 | 22 | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 23 | - | FolderID sql.NullInt64 `json:"folder_id"` | |
| 24 | 23 | } | |
| 25 | 24 | ||
| 26 | 25 | 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) | |
| 33 | 27 | var i Favorite | |
| 34 | 28 | err := row.Scan( | |
| 35 | 29 | &i.ID, | |
| @@ -154,12 +148,15 @@ func (q *Queries) GetFavoriteByURL(ctx context.Context, url string) (Favorite, e | |||
| 154 | 148 | return i, err | |
| 155 | 149 | } | |
| 156 | 150 | ||
| 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 | |
| 159 | 156 | ` | |
| 160 | 157 | ||
| 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) | |
| 163 | 160 | if err != nil { | |
| 164 | 161 | return nil, err | |
| 165 | 162 | } | |
| @@ -191,57 +188,6 @@ func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt | |||
| 191 | 188 | 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 | 191 | const SetFavoriteShortcut = `-- name: SetFavoriteShortcut :exec | |
| 246 | 192 | UPDATE favorites SET shortcut_key = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? | |
| 247 | 193 | ` | |
| @@ -11,8 +11,8 @@ import ( | |||||
| 11 | ) | 11 | ) | ||
| 12 | 12 | ||||
| 13 | const CreateFavorite = `-- name: CreateFavorite :one | 13 | 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_at | 16 | 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 Favorite | 27 | 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, err | 148 | return i, err | ||
| 155 | } | 149 | } | ||
| 156 | 150 | ||||
| 157 | - | const GetFavoritesByFolder = `-- name: GetFavoritesByFolder :many | 151 | + | 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 ASC | 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 | |||
| 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, err | 161 | return nil, err | ||
| 165 | } | 162 | } | ||
| @@ -191,57 +188,6 @@ func (q *Queries) GetFavoritesByFolder(ctx context.Context, folderID sql.NullInt | |||||
| 191 | return items, nil | 188 | 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 :exec | 191 | 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 { | |||
| 14 | 14 | AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error | |
| 15 | 15 | CapVisitCount(ctx context.Context, arg CapVisitCountParams) error | |
| 16 | 16 | CreateFavorite(ctx context.Context, arg CreateFavoriteParams) (Favorite, error) | |
| 17 | - | CreateFolder(ctx context.Context, arg CreateFolderParams) (FavoriteFolder, error) | |
| 18 | 17 | CreateTag(ctx context.Context, arg CreateTagParams) (FavoriteTag, error) | |
| 19 | 18 | DeleteAllHistory(ctx context.Context) error | |
| 20 | 19 | // Deletes exited browser sessions older than the given cutoff time. | |
| 21 | 20 | DeleteExitedSessionsBefore(ctx context.Context, endedAt sql.NullTime) (int64, error) | |
| 22 | 21 | DeleteFavicon(ctx context.Context, key string) error | |
| 23 | 22 | DeleteFavorite(ctx context.Context, id int64) error | |
| 24 | - | DeleteFolder(ctx context.Context, id int64) error | |
| 25 | 23 | DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error | |
| 26 | 24 | DeleteHistoryByID(ctx context.Context, id int64) error | |
| 27 | 25 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | |
| @@ -35,23 +33,19 @@ type Querier interface { | |||
| 35 | 33 | DeleteZoomLevel(ctx context.Context, domain string) error | |
| 36 | 34 | GetActiveBrowserSession(ctx context.Context) (Session, error) | |
| 37 | 35 | GetAllFavorites(ctx context.Context) ([]Favorite, error) | |
| 38 | - | GetAllFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 39 | 36 | GetAllMostVisited(ctx context.Context) ([]History, error) | |
| 40 | 37 | GetAllRecentHistory(ctx context.Context) ([]History, error) | |
| 41 | 38 | GetAllRecentHistoryByDomain(ctx context.Context, domain sql.NullString) ([]History, error) | |
| 42 | 39 | GetAllSessionStates(ctx context.Context) ([]SessionState, error) | |
| 43 | 40 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |
| 44 | 41 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |
| 45 | - | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | |
| 46 | 42 | GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error) | |
| 47 | 43 | GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error) | |
| 48 | 44 | GetFavicon(ctx context.Context, key string) (Favicon, error) | |
| 49 | 45 | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | |
| 50 | 46 | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | |
| 51 | 47 | 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) | |
| 55 | 49 | GetHistoryByURL(ctx context.Context, url string) (History, error) | |
| 56 | 50 | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | |
| 57 | 51 | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | |
| @@ -63,13 +57,13 @@ type Querier interface { | |||
| 63 | 57 | GetRecentHistoryWindow(ctx context.Context, arg GetRecentHistoryWindowParams) ([]History, error) | |
| 64 | 58 | GetRecentHistoryWindowByDomain(ctx context.Context, arg GetRecentHistoryWindowByDomainParams) ([]History, error) | |
| 65 | 59 | GetRecentSessions(ctx context.Context, limit int64) ([]Session, error) | |
| 66 | - | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 67 | 60 | GetSessionByID(ctx context.Context, id string) (Session, error) | |
| 68 | 61 | GetSessionState(ctx context.Context, sessionID string) (SessionState, error) | |
| 69 | 62 | GetSessionsWithState(ctx context.Context, limit int64) ([]GetSessionsWithStateRow, error) | |
| 70 | 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 | 65 | GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]FavoriteTag, error) | |
| 66 | + | GetTagsForFavorites(ctx context.Context, favoriteIds []int64) ([]GetTagsForFavoritesRow, error) | |
| 73 | 67 | GetTotalSessionStatesSize(ctx context.Context) (interface{}, error) | |
| 74 | 68 | GetZoomLevel(ctx context.Context, domain string) (ZoomLevel, error) | |
| 75 | 69 | IncrementVisitCount(ctx context.Context, url string) error | |
| @@ -86,15 +80,12 @@ type Querier interface { | |||
| 86 | 80 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | |
| 87 | 81 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | |
| 88 | 82 | SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) | |
| 89 | - | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |
| 90 | 83 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |
| 91 | 84 | SetPermission(ctx context.Context, arg SetPermissionParams) error | |
| 92 | 85 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |
| 93 | 86 | UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error | |
| 94 | 87 | UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error | |
| 95 | 88 | UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error | |
| 96 | - | UpdateFolder(ctx context.Context, arg UpdateFolderParams) error | |
| 97 | - | UpdateFolderPosition(ctx context.Context, arg UpdateFolderPositionParams) error | |
| 98 | 89 | UpdateTag(ctx context.Context, arg UpdateTagParams) error | |
| 99 | 90 | UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error | |
| 100 | 91 | UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error | |
| @@ -14,14 +14,12 @@ type Querier interface { | |||||
| 14 | AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error | 14 | AssignTagToFavorite(ctx context.Context, arg AssignTagToFavoriteParams) error | ||
| 15 | CapVisitCount(ctx context.Context, arg CapVisitCountParams) error | 15 | 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) error | 18 | 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) error | 21 | DeleteFavicon(ctx context.Context, key string) error | ||
| 23 | DeleteFavorite(ctx context.Context, id int64) error | 22 | DeleteFavorite(ctx context.Context, id int64) error | ||
| 24 | - | DeleteFolder(ctx context.Context, id int64) error | |||
| 25 | DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error | 23 | DeleteHistoryByDomain(ctx context.Context, domain sql.NullString) error | ||
| 26 | DeleteHistoryByID(ctx context.Context, id int64) error | 24 | DeleteHistoryByID(ctx context.Context, id int64) error | ||
| 27 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | 25 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | ||
| @@ -35,23 +33,19 @@ type Querier interface { | |||||
| 35 | DeleteZoomLevel(ctx context.Context, domain string) error | 33 | 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) error | 69 | 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) error | 83 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | ||
| 91 | SetPermission(ctx context.Context, arg SetPermissionParams) error | 84 | SetPermission(ctx context.Context, arg SetPermissionParams) error | ||
| 92 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | 85 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | ||
| 93 | UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error | 86 | UpdateFaviconLastChecked(ctx context.Context, arg UpdateFaviconLastCheckedParams) error | ||
| 94 | UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error | 87 | UpdateFavorite(ctx context.Context, arg UpdateFavoriteParams) error | ||
| 95 | UpdateFavoritePosition(ctx context.Context, arg UpdateFavoritePositionParams) error | 88 | 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) error | 89 | UpdateTag(ctx context.Context, arg UpdateTagParams) error | ||
| 99 | UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error | 90 | UpsertFavicon(ctx context.Context, arg UpsertFaviconParams) error | ||
| 100 | UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error | 91 | UpsertHistory(ctx context.Context, arg UpsertHistoryParams) error | ||
M internal/infrastructure/persistence/sqlite/sqlc/tags.sql.go +55 -3
| @@ -7,6 +7,7 @@ package sqlc | |||
| 7 | 7 | ||
| 8 | 8 | import ( | |
| 9 | 9 | "context" | |
| 10 | + | "strings" | |
| 10 | 11 | ) | |
| 11 | 12 | ||
| 12 | 13 | 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 | 105 | 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 | |
| 106 | 107 | ` | |
| 107 | 108 | ||
| 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) | |
| 110 | 111 | var i FavoriteTag | |
| 111 | 112 | err := row.Scan( | |
| 112 | 113 | &i.ID, | |
| @@ -152,6 +153,57 @@ func (q *Queries) GetTagsForFavorite(ctx context.Context, favoriteID int64) ([]F | |||
| 152 | 153 | 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 | 207 | const RemoveTagFromFavorite = `-- name: RemoveTagFromFavorite :exec | |
| 156 | 208 | DELETE FROM favorite_tag_assignments WHERE favorite_id = ? AND tag_id = ? | |
| 157 | 209 | ` | |
| @@ -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 :exec | 13 | 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 :one | 105 | 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 |
| 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 FavoriteTag | 111 | 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, nil | 153 | 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 :exec | 207 | 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 | ` | ||