refactor(collections): retire the legacy rules surface — tree-only storage

The pre-tree rules machinery is deleted: no row ever carried a rules
array or a non-default match mode (verified in the dev DB), the web
never read the envelope keys, and the Android app consumes none of it.
Dead code gets no documentation.

Removed:
- PUT /api/collections/:id/match-mode — endpoint, route, and the
  UpdateCollectionMatchMode query. The tree's root mode owns any|all.
- auto_assign_rules / rule_match_mode from every envelope
  (create/list/detail/update), from the create/update request shapes,
  and from the schema: both columns DROPPED (schema.sql carries
  idempotent DROP COLUMN IF EXISTS heals for existing DBs; sqlc
  regenerated).
- The rules CRUD service layer (AddRule/SetRuleEnabled/RemoveRule +
  validateRule + UpdateCollectionRulesJSON), ParseRules /
  EnabledRules / IsSmartCollection, RulesToFilterTree (+ the SSR
  preload branch), the legacy branch of EvaluateSmartMembership, and
  SmartEvalOptions.MatchMode. IsCollectionSmart = tree presence;
  create-path isSmart = collection_type=smart OR a filter_tree.

Kept (live consumers): POST /api/collections/preview still evaluates
rules payloads in memory (the custom-section builder's preview), and
include_auto stays on the list (the Android app sends it).

The custom-section builder's SAVE was already broken — its
stringified auto_assign_rules payload failed request binding (probed
live: 400 invalid request). Create now rejects that payload loudly
with a 400 naming the retirement, instead of silently creating a
hollow static section; custom-section-builder.md carries the flag.
The builder's tree migration remains the flagged follow-up.

Test-side (asymmetry rule — deleted behavior, deleted tests):
TestPodcastSmartCollections rewritten onto trees (mode lives in the
tree root), match-mode subtests and tree_wins_over_legacy_rules
deleted, TestRulesToFilterTree deleted, contract-pin envelope keys
retracted per the contract_pins 2026-10-31 retirement entry. Mirrors
hand-synced: openapi.yaml, bruno create/update, docs/api/*.md,
docs/schema.go, api.d.ts. Full container suite green (155s).
This commit is contained in:
John O'Keefe committed 2026-10-31 19:06:48 -04:00
1 parent ab98015190
commit a86bdac87e
27 files changed
+239 -1093

No files matched your search

-2
View File
@@ -34,8 +34,6 @@ type Collections struct {
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
RuleMatchMode string `db:"rule_match_mode" json:"rule_match_mode"`
FilterTree []byte `db:"filter_tree" json:"filter_tree"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
-14
View File
@@ -599,14 +599,6 @@ type Querier interface {
GetUserSettingByKey(ctx context.Context, arg GetUserSettingByKeyParams) ([]byte, error)
GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]GetUserVisibleLibrariesRow, error)
GetVisibleLibraryMediaCounts(ctx context.Context, userID pgtype.UUID) ([]GetVisibleLibraryMediaCountsRow, error)
// Smart ("dynamic") collections: rules are evaluated LIVE at render, so
// membership comes from the candidate book set instead of collection_items.
// The set is scoped to the requesting user's visible libraries (family
// lens enforced at the source) and live items only — the same filters the
// static collection paths apply. The viewer's progress rides along (plan
// item 2.4): the played/unplayed episode rule evaluates per-user, exactly
// like the hide_finished predicate (percentage >= 0.999 = finished).
GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibleLiveMediaForRulesParams) ([]GetVisibleLiveMediaForRulesRow, error)
HasRecentConflictResolution(ctx context.Context, arg HasRecentConflictResolutionParams) (bool, error)
// Soft-dismiss one row (ownership is checked by the caller against the
// device's user_id before calling this).
@@ -828,12 +820,6 @@ type Querier interface {
UnignoreUnlinkedBook(ctx context.Context, id pgtype.UUID) error
// Update collection
UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error)
// Smart-collection match semantics (plan item 2.4): flip any|all on its
// own channel — the rules page toggle writes exactly this column.
UpdateCollectionMatchMode(ctx context.Context, arg UpdateCollectionMatchModeParams) error
// Replace ONLY the auto_assign_rules document (rule CRUD: read-modify-write
// of the JSONB without touching metadata stale clients did not send).
UpdateCollectionRulesJSON(ctx context.Context, arg UpdateCollectionRulesJSONParams) (Collections, error)
// A2c §2.4: provenance + playlist flags on an existing collection without
// touching the rest of the row.
// (Historical bug, fixed 2026-10-30: this wrote `updated_at = NOW()` but
+26 -253
View File
@@ -671,9 +671,9 @@ func (q *Queries) CreateAutoResolvedSyncConflict(ctx context.Context, arg Create
const CreateCollection = `-- name: CreateCollection :one
INSERT INTO collections (user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, filter_tree)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
INSERT INTO collections (user_id, name, description, color, icon, view_settings, show_on_dashboard, filter_tree)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
`
type CreateCollectionParams struct {
@@ -682,7 +682,6 @@ type CreateCollectionParams struct {
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
FilterTree []byte `db:"filter_tree" json:"filter_tree"`
@@ -697,7 +696,6 @@ func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionPara
arg.Description,
arg.Color,
arg.Icon,
arg.AutoAssignRules,
arg.ViewSettings,
arg.ShowOnDashboard,
arg.FilterTree,
@@ -710,8 +708,6 @@ func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionPara
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -2254,9 +2250,9 @@ func (q *Queries) CreateSyncQueueItem(ctx context.Context, arg CreateSyncQueueIt
}
const CreateSystemCollection = `-- name: CreateSystemCollection :one
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection, auto_assign_rules)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, 'null'::jsonb)
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
RETURNING id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
`
type CreateSystemCollectionParams struct {
@@ -2289,8 +2285,6 @@ func (q *Queries) CreateSystemCollection(ctx context.Context, arg CreateSystemCo
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -4389,7 +4383,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([
}
const GetCollection = `-- name: GetCollection :one
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections WHERE id = $1
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections WHERE id = $1
`
// Get collection
@@ -4403,8 +4397,6 @@ func (q *Queries) GetCollection(ctx context.Context, id pgtype.UUID) (Collection
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -4418,7 +4410,7 @@ func (q *Queries) GetCollection(ctx context.Context, id pgtype.UUID) (Collection
}
const GetCollectionByImportedSeries = `-- name: GetCollectionByImportedSeries :one
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
WHERE user_id = $1
AND view_settings->>'imported_from_series' = $2::text
AND deleted_at IS NULL
@@ -4443,8 +4435,6 @@ func (q *Queries) GetCollectionByImportedSeries(ctx context.Context, arg GetColl
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -4458,7 +4448,7 @@ func (q *Queries) GetCollectionByImportedSeries(ctx context.Context, arg GetColl
}
const GetCollectionByUserAndName = `-- name: GetCollectionByUserAndName :one
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
WHERE user_id = $1 AND name = $2 AND deleted_at IS NULL
LIMIT 1
`
@@ -4478,8 +4468,6 @@ func (q *Queries) GetCollectionByUserAndName(ctx context.Context, arg GetCollect
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -4973,7 +4961,7 @@ func (q *Queries) GetCollectionUpNext(ctx context.Context, arg GetCollectionUpNe
const GetCollectionWithBookCount = `-- name: GetCollectionWithBookCount :one
SELECT
c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.rule_match_mode, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at,
c.id, c.user_id, c.name, c.description, c.color, c.icon, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at,
COUNT(ci.id) as book_count
FROM collections c
LEFT JOIN collection_items ci ON c.id = ci.collection_id
@@ -4988,8 +4976,6 @@ type GetCollectionWithBookCountRow struct {
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
RuleMatchMode string `db:"rule_match_mode" json:"rule_match_mode"`
FilterTree []byte `db:"filter_tree" json:"filter_tree"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
ShowOnDashboard pgtype.Bool `db:"show_on_dashboard" json:"show_on_dashboard"`
@@ -5012,8 +4998,6 @@ func (q *Queries) GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -5028,7 +5012,7 @@ func (q *Queries) GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID
}
const GetCollectionsByUser = `-- name: GetCollectionsByUser :many
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections WHERE user_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections WHERE user_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC
`
// Get collections by user
@@ -5048,8 +5032,6 @@ func (q *Queries) GetCollectionsByUser(ctx context.Context, userID pgtype.UUID)
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -5070,7 +5052,7 @@ func (q *Queries) GetCollectionsByUser(ctx context.Context, userID pgtype.UUID)
}
const GetCollectionsForBook = `-- name: GetCollectionsForBook :many
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.rule_match_mode, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at
FROM collections c
JOIN collection_items ci ON c.id = ci.collection_id
WHERE ci.media_item_id = $1
@@ -5094,8 +5076,6 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -10801,7 +10781,7 @@ func (q *Queries) GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (
const GetSystemCollectionByQueryType = `-- name: GetSystemCollectionByQueryType :one
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
WHERE user_id = $1
AND query_type = $2
AND is_system_collection = true
@@ -10827,8 +10807,6 @@ func (q *Queries) GetSystemCollectionByQueryType(ctx context.Context, arg GetSys
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -10842,7 +10820,7 @@ func (q *Queries) GetSystemCollectionByQueryType(ctx context.Context, arg GetSys
}
const GetSystemCollectionsForDashboard = `-- name: GetSystemCollectionsForDashboard :many
SELECT id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
SELECT id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at FROM collections
WHERE user_id = $1
AND is_system_collection = true
AND show_on_dashboard = true
@@ -10867,8 +10845,6 @@ func (q *Queries) GetSystemCollectionsForDashboard(ctx context.Context, userID p
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -11575,7 +11551,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (GetUs
}
const GetUserCollectionsForDashboard = `-- name: GetUserCollectionsForDashboard :many
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.rule_match_mode, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at FROM collections c
SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.filter_tree, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, c.deleted_at FROM collections c
WHERE c.user_id = $1
AND c.is_system_collection = false
AND c.deleted_at IS NULL
@@ -11602,8 +11578,6 @@ func (q *Queries) GetUserCollectionsForDashboard(ctx context.Context, userID pgt
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -12364,146 +12338,6 @@ func (q *Queries) GetVisibleLibraryMediaCounts(ctx context.Context, userID pgtyp
return items, nil
}
const GetVisibleLiveMediaForRules = `-- name: GetVisibleLiveMediaForRules :many
SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, mi.download_queue_pos, COALESCE(rp.percentage, 0) AS viewer_percentage FROM media_items mi
LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = $1::uuid
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
AND ($3::uuid[] IS NULL OR mi.library_id = ANY($3::uuid[]))
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.title ASC
`
type GetVisibleLiveMediaForRulesParams struct {
ViewerUserID pgtype.UUID `db:"viewer_user_id" json:"viewer_user_id"`
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
VisibleLibraryIds []pgtype.UUID `db:"visible_library_ids" json:"visible_library_ids"`
}
type GetVisibleLiveMediaForRulesRow struct {
MediaItems MediaItems `db:"media_items" json:"media_items"`
ViewerPercentage float64 `db:"viewer_percentage" json:"viewer_percentage"`
}
// Smart ("dynamic") collections: rules are evaluated LIVE at render, so
// membership comes from the candidate book set instead of collection_items.
// The set is scoped to the requesting user's visible libraries (family
// lens enforced at the source) and live items only — the same filters the
// static collection paths apply. The viewer's progress rides along (plan
// item 2.4): the played/unplayed episode rule evaluates per-user, exactly
// like the hide_finished predicate (percentage >= 0.999 = finished).
func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibleLiveMediaForRulesParams) ([]GetVisibleLiveMediaForRulesRow, error) {
rows, err := q.db.Query(ctx, GetVisibleLiveMediaForRules, arg.ViewerUserID, arg.LibraryID, arg.VisibleLibraryIds)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetVisibleLiveMediaForRulesRow{}
for rows.Next() {
var i GetVisibleLiveMediaForRulesRow
if err := rows.Scan(
&i.MediaItems.ID,
&i.MediaItems.LibraryID,
&i.MediaItems.Title,
&i.MediaItems.Subtitle,
&i.MediaItems.Author,
&i.MediaItems.Isbn,
&i.MediaItems.Description,
&i.MediaItems.FilePath,
&i.MediaItems.FileSize,
&i.MediaItems.MimeType,
&i.MediaItems.CoverImagePath,
&i.MediaItems.Series,
&i.MediaItems.SeriesNumber,
&i.MediaItems.Tags,
&i.MediaItems.Narrators,
&i.MediaItems.Authors,
&i.MediaItems.AuthorsSearch,
&i.MediaItems.Publishers,
&i.MediaItems.PublishersSearch,
&i.MediaItems.Abridged,
&i.MediaItems.Explicit,
&i.MediaItems.Asin,
&i.MediaItems.DatePublished,
&i.MediaItems.Publisher,
&i.MediaItems.Contributors,
&i.MediaItems.Language,
&i.MediaItems.Edition,
&i.MediaItems.PageCount,
&i.MediaItems.Genre,
&i.MediaItems.CopyrightYear,
&i.MediaItems.GoodreadsID,
&i.MediaItems.OpenlibraryID,
&i.MediaItems.GoogleBooksID,
&i.MediaItems.AddedByAdminID,
&i.MediaItems.CreatedAt,
&i.MediaItems.ImportedAt,
&i.MediaItems.UpdatedAt,
&i.MediaItems.FormatGroup,
&i.MediaItems.FormatMimetype,
&i.MediaItems.IsReflowable,
&i.MediaItems.HasFixedLayout,
&i.MediaItems.TotalCharacters,
&i.MediaItems.ChapterCount,
&i.MediaItems.EntitlementID,
&i.MediaItems.RevisionNumber,
&i.MediaItems.KoboContentID,
&i.MediaItems.KoboMetadata,
&i.MediaItems.MangaType,
&i.MediaItems.ReadingDirection,
&i.MediaItems.SeriesCount,
&i.MediaItems.Volume,
&i.MediaItems.Imprint,
&i.MediaItems.AgeRating,
&i.MediaItems.WebUrl,
&i.MediaItems.StoryArc,
&i.MediaItems.IsBlackAndWhite,
&i.MediaItems.MetadataNotes,
&i.MediaItems.CommunityRating,
&i.MediaItems.AlternateInfo,
&i.MediaItems.ScanInformation,
&i.MediaItems.Summary,
&i.MediaItems.MetadataOverrides,
&i.MediaItems.MissingScanCount,
&i.MediaItems.ArchivedAt,
&i.MediaItems.ChapterMetadata,
&i.MediaItems.LibraryTypeName,
&i.MediaItems.AudioMetadata,
&i.MediaItems.TagsSearch,
&i.MediaItems.ContributorsSearch,
&i.MediaItems.NarratorsSearch,
&i.MediaItems.FileSha256,
&i.MediaItems.OpfIdentifier,
&i.MediaItems.OpfUuid,
&i.MediaItems.HashConfidence,
&i.MediaItems.Source,
&i.MediaItems.SourceFeedID,
&i.MediaItems.EpisodeGuid,
&i.MediaItems.FeedPosition,
&i.MediaItems.SeasonNumber,
&i.MediaItems.EpisodeNumber,
&i.MediaItems.EnclosureUrl,
&i.MediaItems.DeclaredDurationSeconds,
&i.MediaItems.TranscriptUrl,
&i.MediaItems.ChaptersUrl,
&i.MediaItems.Hosts,
&i.MediaItems.Guests,
&i.MediaItems.DownloadState,
&i.MediaItems.DownloadError,
&i.MediaItems.DownloadAttempts,
&i.MediaItems.DownloadQueuePos,
&i.ViewerPercentage,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const HasRecentConflictResolution = `-- name: HasRecentConflictResolution :one
SELECT EXISTS(
SELECT 1 FROM sync_conflicts
@@ -16484,7 +16318,7 @@ SET name = $2,
show_on_dashboard = true,
deleted_at = NULL
WHERE id = $1
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
RETURNING id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
`
type RestoreSystemCollectionRowParams struct {
@@ -16515,8 +16349,6 @@ func (q *Queries) RestoreSystemCollectionRow(ctx context.Context, arg RestoreSys
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
@@ -18230,24 +18062,20 @@ SET
description = $3,
color = $4,
icon = $5,
auto_assign_rules = $6,
view_settings = $7,
rule_match_mode = $8,
filter_tree = $9
view_settings = $6,
filter_tree = $7
WHERE id = $1
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
RETURNING id, user_id, name, description, color, icon, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
`
type UpdateCollectionParams struct {
ID pgtype.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
RuleMatchMode string `db:"rule_match_mode" json:"rule_match_mode"`
FilterTree []byte `db:"filter_tree" json:"filter_tree"`
ID pgtype.UUID `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Description pgtype.Text `db:"description" json:"description"`
Color pgtype.Text `db:"color" json:"color"`
Icon pgtype.Text `db:"icon" json:"icon"`
ViewSettings []byte `db:"view_settings" json:"view_settings"`
FilterTree []byte `db:"filter_tree" json:"filter_tree"`
}
// Update collection
@@ -18258,9 +18086,7 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara
arg.Description,
arg.Color,
arg.Icon,
arg.AutoAssignRules,
arg.ViewSettings,
arg.RuleMatchMode,
arg.FilterTree,
)
var i Collections
@@ -18271,59 +18097,6 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
&i.Priority,
&i.IsSystemCollection,
&i.CreatedAt,
&i.DeletedAt,
)
return i, err
}
const UpdateCollectionMatchMode = `-- name: UpdateCollectionMatchMode :exec
UPDATE collections SET rule_match_mode = $2 WHERE id = $1
`
type UpdateCollectionMatchModeParams struct {
ID pgtype.UUID `db:"id" json:"id"`
RuleMatchMode string `db:"rule_match_mode" json:"rule_match_mode"`
}
// Smart-collection match semantics (plan item 2.4): flip any|all on its
// own channel — the rules page toggle writes exactly this column.
func (q *Queries) UpdateCollectionMatchMode(ctx context.Context, arg UpdateCollectionMatchModeParams) error {
_, err := q.db.Exec(ctx, UpdateCollectionMatchMode, arg.ID, arg.RuleMatchMode)
return err
}
const UpdateCollectionRulesJSON = `-- name: UpdateCollectionRulesJSON :one
UPDATE collections SET auto_assign_rules = $2 WHERE id = $1 RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, filter_tree, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
`
type UpdateCollectionRulesJSONParams struct {
ID pgtype.UUID `db:"id" json:"id"`
AutoAssignRules []byte `db:"auto_assign_rules" json:"auto_assign_rules"`
}
// Replace ONLY the auto_assign_rules document (rule CRUD: read-modify-write
// of the JSONB without touching metadata stale clients did not send).
func (q *Queries) UpdateCollectionRulesJSON(ctx context.Context, arg UpdateCollectionRulesJSONParams) (Collections, error) {
row := q.db.QueryRow(ctx, UpdateCollectionRulesJSON, arg.ID, arg.AutoAssignRules)
var i Collections
err := row.Scan(
&i.ID,
&i.UserID,
&i.Name,
&i.Description,
&i.Color,
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
+6 -34
View File
@@ -3489,8 +3489,8 @@ DELETE FROM device_file_aliases WHERE id = $1;
-- Create collection
-- name: CreateCollection :one
INSERT INTO collections (user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, filter_tree)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
INSERT INTO collections (user_id, name, description, color, icon, view_settings, show_on_dashboard, filter_tree)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *;
-- A2c §2.4: provenance + playlist flags on an existing collection without
@@ -3502,11 +3502,6 @@ RETURNING *;
-- dropping the phantom column write.)
UPDATE collections SET view_settings = $2 WHERE id = $1;
-- Smart-collection match semantics (plan item 2.4): flip any|all on its
-- own channel — the rules page toggle writes exactly this column.
-- name: UpdateCollectionMatchMode :exec
UPDATE collections SET rule_match_mode = $2 WHERE id = $1;
-- Get collection
-- name: GetCollection :one
SELECT * FROM collections WHERE id = $1;
@@ -3533,18 +3528,11 @@ SET
description = $3,
color = $4,
icon = $5,
auto_assign_rules = $6,
view_settings = $7,
rule_match_mode = $8,
filter_tree = $9
view_settings = $6,
filter_tree = $7
WHERE id = $1
RETURNING *;
-- Replace ONLY the auto_assign_rules document (rule CRUD: read-modify-write
-- of the JSONB without touching metadata stale clients did not send).
-- name: UpdateCollectionRulesJSON :one
UPDATE collections SET auto_assign_rules = $2 WHERE id = $1 RETURNING *;
-- Delete collection
-- name: DeleteCollection :exec
DELETE FROM collections WHERE id = $1;
@@ -4034,8 +4022,8 @@ RETURNING *;
DELETE FROM collection_items WHERE collection_id = $1;
-- name: CreateSystemCollection :one
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection, auto_assign_rules)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, 'null'::jsonb)
INSERT INTO collections (user_id, name, description, icon, color, show_on_dashboard, query_type, priority, is_system_collection)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
RETURNING *;
-- name: ResetSystemCollectionMetadata :exec
@@ -4151,22 +4139,6 @@ LIMIT sqlc.narg('limit');
-- name: CountCollectionItems :one
SELECT COUNT(*) FROM collection_items WHERE collection_id = $1;
-- Smart ("dynamic") collections: rules are evaluated LIVE at render, so
-- membership comes from the candidate book set instead of collection_items.
-- The set is scoped to the requesting user's visible libraries (family
-- lens enforced at the source) and live items only — the same filters the
-- static collection paths apply. The viewer's progress rides along (plan
-- item 2.4): the played/unplayed episode rule evaluates per-user, exactly
-- like the hide_finished predicate (percentage >= 0.999 = finished).
-- name: GetVisibleLiveMediaForRules :many
SELECT sqlc.embed(mi), COALESCE(rp.percentage, 0) AS viewer_percentage FROM media_items mi
LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = sqlc.narg('viewer_user_id')::uuid
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
AND (sqlc.narg('visible_library_ids')::uuid[] IS NULL OR mi.library_id = ANY(sqlc.narg('visible_library_ids')::uuid[]))
AND mi.archived_at IS NULL
AND mi.missing_scan_count = 0
ORDER BY mi.title ASC;
-- Live item count for a STATIC collection (rule-free cards on the
-- collections page); same live filter as GetCollectionItems, plus the
-- future-hybrid excluded exclusion and an optional library scope.