As a 3rd-year computer science student, jumping into a massive, production-grade open-source project is incredibly intimidating. When I first set up my build environment for Mixxx (the open-source DJ software) in VS Code, just getting it to compile felt like a victory. But I wanted to make a real contribution, which meant picking up a real issue.
I ended up tackling a bug that looked like spooky action at a distance: deleting a history playlist was somehow erasing metadata from tracks in the main library.
Here is the story of how I tracked down the issue, learned how Mixxx handles its SQLite database under the hood, and pushed a fix.
The Weird Behavior
Imagine you are a DJ. You play a track on Friday night. Mixxx records the play count and logs a last_played_at timestamp for that track in your main library. Everything is working perfectly.
A few weeks later, you decide to clean up your workspace and delete the history playlist from that Friday night gig. You’d expect the history log to disappear, but the track in your main library should still remember that you’ve played it before, right?
Wrong. For some users, deleting that old history playlist caused the last_played_at timestamp on those tracks to vanish completely. It was as if the tracks had never been played.
Why would deleting a temporary history list corrupt the persistent metadata of the main library?
Following the Trail
To figure this out, I had to understand how Mixxx actually stores this data. I started tracing the code from the UI down into the data access layer.
Mixxx uses a Data Access Object (DAO) pattern to interface between the C++ application and the underlying SQLite database. The main library data lives in a library table, but the history is managed through Playlists and PlaylistTracks.
When a user deletes a history playlist, Mixxx doesn't just drop the rows. It tries to be smart and synchronize the play statistics for the tracks that were in that deleted history. The logic for this lived in src/library/dao/trackdao.cpp, specifically inside a function called updatePlayCounterFromPlayedHistory().
Staring at trackdao.cpp for the first time was overwhelming, but eventually, the flow started to make sense. When a history list was deleted, the application asked TrackDAO to recalculate the track's history.
Finding the Culprit
The root of the bug came down to how SQL and C++ handle "nothing."
To figure out the last time a track was played, TrackDAO was executing an SQL query across the history tables, looking for MAX(pl_datetime_added) where the playlist type was a history log (PlaylistDAO::PLHT_SET_LOG).
Here was the fatal flaw: if a user deleted the only history playlists that contained a specific track, that track no longer had any history records left in the database.
When the SQL query ran MAX(pl_datetime_added) for that TrackId, it found nothing. In C++ (using Qt), this resulted in a QSqlQuery returning an invalid or empty QDateTime object.
Instead of recognizing that the history was just missing and leaving the main library alone, the old code was taking that invalid QDateTime and blindly updating the last_played_at column in the main library table. It was overwriting perfectly good timestamps with "null."
The Fix
The solution was to isolate the history lookup and add some safety checks so we never overwrite good data with bad data.
I modified the logic in TrackDAO and created a dedicated helper function: findLastTimeAddedToHistory(TrackId trackId).
This function encapsulates the QSqlQuery (using Mixxx's ScopedQuery for safety) to cleanly fetch the latest timestamp from the PlaylistTracks and Playlists tables.
Instead of mixing the database query directly into the update logic, updatePlayCounterFromPlayedHistory now simply calls my helper. More importantly, it checks the result. If findLastTimeAddedToHistory returns an invalid QDateTime (meaning there is no history left for that track), we gracefully handle it instead of wiping out the existing last_played_at data in the main library.
What I Learned
When I first started looking at Issue #14427, I thought I was going to have to rewrite massive chunks of the database architecture.
What I actually learned is that in a mature codebase, bugs rarely require you to tear down the walls. It’s usually about finding the exact point where data flows from one system to another—in this case, from an SQLite query into a Qt QDateTime object—and realizing an edge case wasn't accounted for.
By extracting the database query into its own cleanly scoped helper function, not only did the bug get fixed, but TrackDAO became just a little bit easier for the next student or contributor to read.
I pushed my fix in PR #16178 to isolate the database query.
This strange behavior was originally reported in Issue #14427.
Top comments (0)