Switch the 500-response stubs to Req.Test.json so Req decodes the
body, then match the map shape. Avoids coupling the assertion to
the exact JSON serialization.
Refs #176
Repo.delete always returns the struct it was given, so deleted.id
== original.id is trivially true. Thread deleted.id through the
trailing get_*! call instead, so the binding is load-bearing.
Refs #176
Pin the duplicate-rule error to the match_value field and "has already been
taken" message, matching the unique_constraint error_key in the changeset.
Refs #176
Pin invalid-attrs error to specific field, assert collection UUID round-trips
through Ecto.UUID.cast unchanged, and assert deleted.id == chat.id on delete.
Refs #176
Assert deleted.id == set.id on delete, and pin the duplicate error to the
specific field and message from the unique_constraint on record_set_id.
Refs #176
Bind returned embeddings and assert record_id / text_representation match.
Pin OpenAI error path to the actual raw string body returned by the stub.
Refs #176
Replace 9 `{:ok, _}` sites with specific response-body checks to verify
the context returns the Last.fm API payload and not just the success branch.
Refs #176
Covers all action handlers on LiveHelpers.RecordActions and all
update/event clauses on Components.Chat — including streaming
chunk/done/error transitions and the retry re-send path.
Closes#179
SQLite interprets bare years (e.g. "1970") as Julian day numbers
in strftime, producing wrong month-day matches. Filter to only
full YYYY-MM-DD dates before comparing.
Replace scalar collected_record_id/wishlisted_record_id subqueries with
a single matching_records JSON array grouped by release group
(records.musicbrainz_id). Add parse_matching_records/1 for JSON parsing
and derive_legacy_record_ids/2 as a temporary bridge for existing
templates. Update tests to use shared musicbrainz_id fixtures.
Replaces collected_record_id and wishlisted_record_id correlated
subqueries in tracks_with_record_info_query with a single
matching_records json_group_array subquery that returns all records
sharing the same release ID. Adds parse_matching_records/1 for JSON
deserialization. Updates recent_activity/2 and list_tracks/1 to parse
the JSON at the boundary. Derives legacy collected_record_id and
wishlisted_record_id from matching_records so existing LiveViews keep
working. The get_top_albums test is temporarily skipped pending the
top_albums_attach_metadata update.
The delta branch in decode_responses_event now explicitly returns
:ok after invoking the callback, so a callback returning {:error, _}
cannot be mistaken for a stream-level error by decode_events.
The record_count now reflects the number of grouped catalog lines
the model sees, not raw DB rows. Two copies of the same album on
different formats count as 1 catalog entry.
Three independent refactors of lib/music_library/listening_stats.ex,
measured against the dev DB (104k tracks) via bench/listening_stats.exs:
| Query | Baseline | After | Speedup |
|---------------------------------|-----------|-----------|---------|
| recent_activity(tz, 100) | 38.98 ms | 2.87 ms | 13.6x |
| list_tracks(page 1, 200) | 47.30 ms | 4.81 ms | 9.8x |
| get_top_artists_by_days(7) | 66.51 ms | 1.02 ms | 65x |
| get_top_artists_by_days(30) | 67.41 ms | 2.84 ms | 23.7x |
| get_top_artists_by_days(365) | 76.03 ms | 25.57 ms | 3.0x |
| get_top_albums_by_days(7) | 92.63 ms | 1.61 ms | 57.5x |
| get_top_albums_by_days(30) | 94.52 ms | 3.69 ms | 25.6x |
| get_top_albums_by_days(365) | 105.23 ms | 32.81 ms | 3.2x |
| get_top_artists(limit: 10) | 123.96 ms | 101.68 ms | 1.22x |
| get_top_albums(limit: 10) | 209.64 ms | 108.23 ms | 1.94x |
tracks_with_record_info_query/0 now uses correlated scalar subqueries
against record_releases and artist_records instead of materializing
helper subqueries on every call. The cost scales with the outer LIMIT,
not with the size of record_releases.
top_albums_base_query/0 and top_artists_base_query/0 are replaced by
aggregate_query + attach_metadata pairs. The pattern is aggregate first,
attach metadata second: GROUP BY runs against the raw track scan, then
a tiny outer SELECT attaches record_releases / artist_infos lookups for
the <= 10 result rows via correlated subqueries.
tracks_since_query/1 wraps the date-filtered inner scan with limit: -1.
SQLite cannot flatten a subquery that has a LIMIT, so it materializes
the date-bounded subset and the optimizer uses the timestamp index for
the range scan instead of the album/artist composite index. This trick
replaces SQLite's WITH ... AS MATERIALIZED (which ecto_sqlite3 doesn't
expose).
All json_extract(?, '\$.path') fragments use the canonical form rather
than the equivalent ? ->> '\$.path' shorthand. SQLite's index matcher
requires the GROUP BY expression to match the index expression
textually to use the composite index for natural ordering.
collected_releases_query/0 and wishlisted_releases_query/0 are removed
from Collection and Wishlist — they had a single internal caller that
no longer exists after the refactor.
New regression tests lock the semantics that the optimized queries
must preserve:
- count(DISTINCT scrobbled_at_uts) — 579 duplicate timestamps exist in
the dev DB from rapid Last.fm scrobbles, so replacing with count(*)
would silently change results
- :artist_id key in list_tracks result maps — ScrobbledTracksLive.Index
destructures it even though the template body never references it
Closes#148