diff --git a/internal/db/dbq/library_changes.sql.go b/internal/db/dbq/library_changes.sql.go new file mode 100644 index 00000000..09256c2a --- /dev/null +++ b/internal/db/dbq/library_changes.sql.go @@ -0,0 +1,87 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: library_changes.sql + +package dbq + +import ( + "context" +) + +const getLibraryChangesSince = `-- name: GetLibraryChangesSince :many +SELECT id, entity_type, entity_id, op, changed_at +FROM library_changes +WHERE id > $1 +ORDER BY id ASC +LIMIT $2 +` + +type GetLibraryChangesSinceParams struct { + ID int64 + Limit int32 +} + +func (q *Queries) GetLibraryChangesSince(ctx context.Context, arg GetLibraryChangesSinceParams) ([]LibraryChange, error) { + rows, err := q.db.Query(ctx, getLibraryChangesSince, arg.ID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []LibraryChange + for rows.Next() { + var i LibraryChange + if err := rows.Scan( + &i.ID, + &i.EntityType, + &i.EntityID, + &i.Op, + &i.ChangedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getMaxLibraryChangeCursor = `-- name: GetMaxLibraryChangeCursor :one +SELECT COALESCE(MAX(id), 0)::BIGINT FROM library_changes +` + +func (q *Queries) GetMaxLibraryChangeCursor(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, getMaxLibraryChangeCursor) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} + +const getMinLibraryChangeCursor = `-- name: GetMinLibraryChangeCursor :one +SELECT COALESCE(MIN(id), 0)::BIGINT FROM library_changes +` + +func (q *Queries) GetMinLibraryChangeCursor(ctx context.Context) (int64, error) { + row := q.db.QueryRow(ctx, getMinLibraryChangeCursor) + var column_1 int64 + err := row.Scan(&column_1) + return column_1, err +} + +const insertLibraryChange = `-- name: InsertLibraryChange :exec +INSERT INTO library_changes (entity_type, entity_id, op) +VALUES ($1, $2, $3) +` + +type InsertLibraryChangeParams struct { + EntityType string + EntityID string + Op string +} + +func (q *Queries) InsertLibraryChange(ctx context.Context, arg InsertLibraryChangeParams) error { + _, err := q.db.Exec(ctx, insertLibraryChange, arg.EntityType, arg.EntityID, arg.Op) + return err +} diff --git a/internal/db/dbq/models.go b/internal/db/dbq/models.go index 22f6fcec..2a50b419 100644 --- a/internal/db/dbq/models.go +++ b/internal/db/dbq/models.go @@ -283,6 +283,14 @@ type GeneralLikesArtist struct { LikedAt pgtype.Timestamptz } +type LibraryChange struct { + ID int64 + EntityType string + EntityID string + Op string + ChangedAt pgtype.Timestamptz +} + type LidarrConfig struct { ID int16 Enabled bool diff --git a/internal/db/migrations/0025_library_changes.down.sql b/internal/db/migrations/0025_library_changes.down.sql new file mode 100644 index 00000000..fed8a61a --- /dev/null +++ b/internal/db/migrations/0025_library_changes.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS library_changes; diff --git a/internal/db/migrations/0025_library_changes.up.sql b/internal/db/migrations/0025_library_changes.up.sql new file mode 100644 index 00000000..770c2c8a --- /dev/null +++ b/internal/db/migrations/0025_library_changes.up.sql @@ -0,0 +1,23 @@ +-- Append-only change log for library entities. Every mutation on +-- artists/albums/tracks/likes/playlists/playlist_tracks writes a row +-- in the same transaction as the mutation itself. Powers the Flutter +-- delta-sync endpoint (#357) — clients pass back the last-seen `id` +-- as a cursor. +-- +-- Retention: not capped here. If the table grows unbounded a follow-up +-- adds a periodic compactor (collapse contiguous upserts on the same +-- entity, drop pre-delete history). Cursor reset path (410) covers the +-- compactor's edge. + +CREATE TABLE library_changes ( + id BIGSERIAL PRIMARY KEY, + entity_type TEXT NOT NULL CHECK (entity_type IN ( + 'artist', 'album', 'track', 'like_track', 'like_album', 'like_artist', + 'playlist', 'playlist_track' + )), + entity_id TEXT NOT NULL, + op TEXT NOT NULL CHECK (op IN ('upsert', 'delete')), + changed_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_library_changes_id ON library_changes(id); diff --git a/internal/db/queries/library_changes.sql b/internal/db/queries/library_changes.sql new file mode 100644 index 00000000..d830c469 --- /dev/null +++ b/internal/db/queries/library_changes.sql @@ -0,0 +1,16 @@ +-- name: InsertLibraryChange :exec +INSERT INTO library_changes (entity_type, entity_id, op) +VALUES ($1, $2, $3); + +-- name: GetLibraryChangesSince :many +SELECT id, entity_type, entity_id, op, changed_at +FROM library_changes +WHERE id > $1 +ORDER BY id ASC +LIMIT $2; + +-- name: GetMaxLibraryChangeCursor :one +SELECT COALESCE(MAX(id), 0)::BIGINT FROM library_changes; + +-- name: GetMinLibraryChangeCursor :one +SELECT COALESCE(MIN(id), 0)::BIGINT FROM library_changes;