feat(server): library_changes log table + sqlc queries
Append-only change log for library entities. Every mutation on artists/albums/tracks/likes/playlists/playlist_tracks will write a row in the same transaction as the mutation itself (wired in subsequent commits). Powers the Flutter delta-sync endpoint (#357). - 0025_library_changes migration (up + down) - internal/db/queries/library_changes.sql (Insert, GetSince, MaxCursor, MinCursor) - regenerated dbq from sqlc Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS library_changes;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user