diff --git a/.jules/bolt.md b/.jules/bolt.md index 1fe6484..a5c8602 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,3 +1,11 @@ ## 2026-02-10 - Atomic Database Updates **Learning:** Read-modify-write patterns for counters (like `playCount`) cause race conditions and extra DB round trips. **Action:** Use atomic SQL updates (e.g., `playCount = playCount + 1`) with `returning()` to ensure data integrity and performance. + +## 2026-03-01 - Join Optimization +**Learning:** Fetching related items (like songs in a playlist) by first fetching IDs and then fetching items (N+1-ish) is inefficient and complex to sort manually. +**Action:** Use `innerJoin` with `orderBy` on the join table to fetch related items in a single query with correct ordering, reducing round trips and code complexity. + +## 2026-03-01 - Drizzle nullsLast +**Learning:** The `.nullsLast()` method on `desc()` may not be available or cause type errors in some Drizzle versions/configurations. +**Action:** Remove `.nullsLast()` if the column is effectively non-nullable (e.g., `defaultNow()`), or use `sql` operator if strictly needed. diff --git a/server/storage.ts b/server/storage.ts index c8fc131..ff140b9 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -10,7 +10,7 @@ import { type Playlist, type InsertPlaylist, } from "@shared/schema"; -import { eq, desc, and, inArray, sql, getTableColumns } from "drizzle-orm"; +import { eq, desc, asc, and, inArray, sql, getTableColumns } from "drizzle-orm"; export interface IStorage { // Song CRUD @@ -140,7 +140,7 @@ export class DatabaseStorage implements IStorage { .from(songs) .innerJoin(songLikes, eq(songs.id, songLikes.songId)) .where(eq(songLikes.userId, userId)) - .orderBy(desc(songLikes.createdAt).nullsLast()); + .orderBy(desc(songLikes.createdAt)); } // === Playlists === @@ -160,22 +160,13 @@ export class DatabaseStorage implements IStorage { const playlist = await this.getPlaylist(id); if (!playlist) return undefined; - const playlistSongRows = await db.select({ songId: playlistSongs.songId }) - .from(playlistSongs) - .where(eq(playlistSongs.playlistId, id)); - - const songIds = playlistSongRows.map(r => r.songId).filter(id => typeof id === 'number' && !isNaN(id)); - - if (songIds.length === 0) { - return { ...playlist, songs: [] }; - } - - const songsResult = await db.select() + // Optimized: Single query with innerJoin to fetch songs directly, ordered by added time + // Replaces 3 separate queries (playlist -> IDs -> songs) and manual sorting + const songsList = await db.select(getTableColumns(songs)) .from(songs) - .where(inArray(songs.id, songIds)); - - const songMap = new Map(songsResult.map(s => [s.id, s])); - const songsList = songIds.map(id => songMap.get(id)).filter((s): s is Song => !!s); + .innerJoin(playlistSongs, eq(songs.id, playlistSongs.songId)) + .where(eq(playlistSongs.playlistId, id)) + .orderBy(asc(playlistSongs.addedAt)); return { ...playlist, songs: songsList }; }