feat(podcasts): smart collections ride the unified filter engine — filter_tree column (item 3.2)

collections gains filter_tree JSONB (idempotent in-place; same qf DSL
as saved filters — the scope key included). IsCollectionSmart is the
ONE detection (tree OR legacy rules), and EvaluateSmartMembership is
the ONE evaluator: tree-bearing collections run the composed engine
via ComposedLiveMedia (full media rows, so the collection surfaces
keep their episode-card stamping, feed positions for the replay
chain, and the family lens rides the mandatory head), while legacy
rule arrays keep per-item FieldMatches until rewritten — one call
surface, two storage epochs. All smart surfaces (detail JSON, counts,
detail page, reader chain) dispatch through it; the search service is
injected into the collection subsystem (variadic — main.go builds one
shared instance; the test harness mirrors it).

Write path: CreateCollection/UpdateCollection accept the tree,
validated against the engine grammar at the HANDLER (an invalid tree
is a 400, never a stored render-time 500) and exposed in the envelope
(pin widened in TestContractPin_CollectionsJSON + changelog). Update
semantics: an edit form that doesn't send a tree keeps the stored one.

RulesToFilterTree converts legacy rules + match mode at upgrade time:
episode rules map mechanically (feed→in-list, played/downloaded→
booleans, published_within→published_date+within_days), tree root
mode absorbs rule_match_mode, episode rules force the episodes scope,
and legacy starts_with/ends_with fall back to contains (documented).

Read-compat by design: nothing is migrated at boot — legacy arrays
evaluate exactly as before until the collection is rewritten.

Pinned by TestRulesToFilterTree (unit conversion) +
TestEpisodeCollectionsFilterTree (a tree collection evaluates through
the engine, the chain follows the tree's live membership, invalid
trees 400 at write, the tree wins when legacy rules coexist, and an
edit without a tree keeps it).
This commit is contained in:
John O'Keefe committed 2026-10-30 19:02:49 -04:00
1 parent 56871b65ed
commit 6b2125a45a
17 files changed
+524 -55

No files matched your search

+37 -17
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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
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
`
type CreateCollectionParams struct {
@@ -685,6 +685,7 @@ type CreateCollectionParams struct {
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"`
}
// COLLECTIONS QUERIES
@@ -699,6 +700,7 @@ func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionPara
arg.AutoAssignRules,
arg.ViewSettings,
arg.ShowOnDashboard,
arg.FilterTree,
)
var i Collections
err := row.Scan(
@@ -710,6 +712,7 @@ func (q *Queries) CreateCollection(ctx context.Context, arg CreateCollectionPara
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -2253,7 +2256,7 @@ 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, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
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 CreateSystemCollectionParams struct {
@@ -2288,6 +2291,7 @@ func (q *Queries) CreateSystemCollection(ctx context.Context, arg CreateSystemCo
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -4385,7 +4389,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, 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, 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
`
// Get collection
@@ -4401,6 +4405,7 @@ func (q *Queries) GetCollection(ctx context.Context, id pgtype.UUID) (Collection
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -4413,7 +4418,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, 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, 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 view_settings->>'imported_from_series' = $2::text
AND deleted_at IS NULL
@@ -4440,6 +4445,7 @@ func (q *Queries) GetCollectionByImportedSeries(ctx context.Context, arg GetColl
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -4452,7 +4458,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, 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, 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 name = $2 AND deleted_at IS NULL
LIMIT 1
`
@@ -4474,6 +4480,7 @@ func (q *Queries) GetCollectionByUserAndName(ctx context.Context, arg GetCollect
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -4966,7 +4973,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.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.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,
COUNT(ci.id) as book_count
FROM collections c
LEFT JOIN collection_items ci ON c.id = ci.collection_id
@@ -4983,6 +4990,7 @@ type GetCollectionWithBookCountRow struct {
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"`
QueryType pgtype.Text `db:"query_type" json:"query_type"`
@@ -5006,6 +5014,7 @@ func (q *Queries) GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -5019,7 +5028,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, 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, 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
`
// Get collections by user
@@ -5041,6 +5050,7 @@ func (q *Queries) GetCollectionsByUser(ctx context.Context, userID pgtype.UUID)
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -5060,7 +5070,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.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.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
JOIN collection_items ci ON c.id = ci.collection_id
WHERE ci.media_item_id = $1
@@ -5086,6 +5096,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -10790,7 +10801,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, 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, 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 query_type = $2
AND is_system_collection = true
@@ -10818,6 +10829,7 @@ func (q *Queries) GetSystemCollectionByQueryType(ctx context.Context, arg GetSys
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -10830,7 +10842,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, 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, 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 is_system_collection = true
AND show_on_dashboard = true
@@ -10857,6 +10869,7 @@ func (q *Queries) GetSystemCollectionsForDashboard(ctx context.Context, userID p
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -11562,7 +11575,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.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.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
WHERE c.user_id = $1
AND c.is_system_collection = false
AND c.deleted_at IS NULL
@@ -11591,6 +11604,7 @@ func (q *Queries) GetUserCollectionsForDashboard(ctx context.Context, userID pgt
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -16470,7 +16484,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, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
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 RestoreSystemCollectionRowParams struct {
@@ -16503,6 +16517,7 @@ func (q *Queries) RestoreSystemCollectionRow(ctx context.Context, arg RestoreSys
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -18217,9 +18232,10 @@ SET
icon = $5,
auto_assign_rules = $6,
view_settings = $7,
rule_match_mode = $8
rule_match_mode = $8,
filter_tree = $9
WHERE id = $1
RETURNING id, user_id, name, description, color, icon, auto_assign_rules, rule_match_mode, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
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 UpdateCollectionParams struct {
@@ -18231,6 +18247,7 @@ type UpdateCollectionParams struct {
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"`
}
// Update collection
@@ -18244,6 +18261,7 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara
arg.AutoAssignRules,
arg.ViewSettings,
arg.RuleMatchMode,
arg.FilterTree,
)
var i Collections
err := row.Scan(
@@ -18255,6 +18273,7 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,
@@ -18283,7 +18302,7 @@ func (q *Queries) UpdateCollectionMatchMode(ctx context.Context, arg UpdateColle
}
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, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at, deleted_at
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 {
@@ -18305,6 +18324,7 @@ func (q *Queries) UpdateCollectionRulesJSON(ctx context.Context, arg UpdateColle
&i.Icon,
&i.AutoAssignRules,
&i.RuleMatchMode,
&i.FilterTree,
&i.ViewSettings,
&i.ShowOnDashboard,
&i.QueryType,