zuletzt/src/queries.zig
mitteneer 162341fb5f Convert ids to i64
The birthday paradox is a real problem with the size of our datasets. i64 is the largest numerical value we can use, and there's a 0.1% chance of collision with ~2,000,000 values, so I feel pretty comfortable with this
2025-06-09 21:41:52 -04:00

424 lines
21 KiB
Zig

// Probably the worst code in the project
const jetzig = @import("jetzig");
const TableRow = @import("types.zig").TableRow;
const HyperlinkData = @import("types.zig").HyperlinkData;
const std = @import("std");
pub fn entityQueryResult(request: *jetzig.Request, query: GeneratedQuery, args: anytype) !*jetzig.Data.Value {
//var result = try request.repo.executeSql(query.query, args);
//
var result = try request.repo.connection.?.postgresql.connection.queryOpts(query.query, args, .{ .allocator = request.allocator, .column_names = true });
defer result.deinit();
var Data = jetzig.Data.init(request.allocator);
var artist_list = std.ArrayList(HyperlinkData).init(request.allocator);
if (query.query_type == .entity_info) {
var out: *jetzig.Data.Value = try Data.object();
const entity = try (try result.next()).?.to(EntityInfoResult, .{ .dupe = true, .allocator = request.allocator, .map = .name });
try out.put("entity_info", entity);
try result.drain();
return out.get("entity_info").?;
}
var out: *jetzig.Data.Value = try Data.array();
var mapper = result.mapper(UnifiedResult, .{ .dupe = true, .allocator = request.allocator });
blk: while (try mapper.next()) |entity| {
if (entity.artist_id) |_| {
const last_artist = artist_list.getLastOrNull();
try artist_list.append(.{ .name = entity.artist_name.?, .id = entity.artist_id.? });
if (last_artist) |la| {
if (la.id == entity.artist_id) continue :blk;
}
}
try out.append(TableRow{
.artist = if (entity.artist_id) |_| .{ .id = entity.artist_id.?, .name = entity.artist_name.? } else null,
.album = if (entity.album_id) |_| .{ .id = entity.album_id.?, .name = entity.album_name.? } else null,
.song = if (entity.song_id) |_| .{ .id = entity.song_id.?, .name = entity.song_name.? } else null,
.artistlist = if (artist_list.getLastOrNull()) |_| try artist_list.toOwnedSlice() else null,
.scrobbles = if (entity.scrobbles) |scrobbles| scrobbles else null,
.date = if (entity.date) |date| date else null,
});
}
return out;
}
const EntityType = enum { scrobble, song, album, artist };
const QueryTypeEnum = enum { firstlast, timescale, entities, get_songs, get_albums, get_scrobbles, appears, entity_info, datestreak };
const GeneratedQuery = struct {
entity: EntityType,
query_type: QueryTypeEnum,
query: []const u8,
};
const UnifiedResult = struct {
album_name: ?[]const u8 = null,
album_id: ?i64 = null,
song_name: ?[]const u8 = null,
song_id: ?i64 = null,
artist_name: ?[]const u8 = null,
artist_id: ?i64 = null,
scrobbles: ?i64 = null,
date: ?[]const u8 = null,
};
const EntityInfoResult = struct {
album_name: ?[]const u8 = null,
album_id: ?i64 = null,
song_name: ?[]const u8 = null,
song_id: ?i64 = null,
artist_name: ?[]const u8 = null,
artist_id: ?i64 = null,
scrobbles: ?i64 = null,
date: ?[]const u8 = null,
rank: []const u8,
song_num: ?i64 = null,
album_num: ?i64 = null,
};
pub fn loadQuery(entity: EntityType, query_type: QueryTypeEnum) GeneratedQuery {
return GeneratedQuery{
.entity = entity,
.query_type = query_type,
.query = switch (query_type) {
.firstlast =>
//.ResultType = FirstlastResult,
switch (entity) {
.scrobble => unreachable,
.song =>
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime,'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\WHERE albumsongs.song_id = $1
\\ORDER BY scrobbles.datetime ASC
\\LIMIT 1)
\\
\\UNION ALL
\\
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\WHERE albumsongs.song_id = $1
\\ORDER BY scrobbles.datetime DESC
\\LIMIT 1)
,
.album =>
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\WHERE albums.id = $1
\\ORDER BY scrobbles.datetime ASC
\\LIMIT 1)
\\
\\UNION ALL
\\
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\WHERE albums.id = $1
\\ORDER BY scrobbles.datetime DESC
\\LIMIT 1)
,
.artist =>
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongsartists.artist_id = $1
\\ORDER BY scrobbles.datetime ASC
\\LIMIT 1)
\\
\\UNION ALL
\\
\\(SELECT songs.name AS song_name, songs.id AS song_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongsartists.artist_id = $1
\\ORDER BY scrobbles.datetime DESC
\\LIMIT 1)
,
},
.timescale =>
//.ResultType = TimescaleResult,
switch (entity) {
.scrobble => unreachable,
.song =>
\\SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
\\FROM scrobbles
\\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\WHERE songs.id = $1
\\GROUP BY date
\\ORDER BY date ASC;
,
.album =>
\\SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
\\FROM scrobbles
\\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\WHERE albums.id = $1
\\GROUP BY date
\\ORDER BY date ASC;
,
.artist =>
\\SELECT y.year AS date, COALESCE(DT.scrobbles, 0) AS scrobbles
\\FROM (SELECT GENERATE_SERIES(2016,date_part('year',now())::int)::text) AS y(year)
\\LEFT JOIN (SELECT TO_CHAR(date_trunc('year', datetime), 'YYYY') AS date, COUNT(*) as scrobbles
\\FROM scrobbles
\\INNER JOIN albumsongs ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN artists ON artists.id = albumsongsartists.artist_id
\\WHERE artists.id = $1
\\GROUP BY date
\\ORDER BY date ASC) AS DT
\\ON DT.date = y.year;
,
},
.entities =>
//.ResultType = EntitiesResult,
switch (entity) {
.scrobble =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN artists ON artists.id = albumsongsartists.artist_id
\\ORDER BY scrobbles.datetime ASC
,
.song =>
\\SELECT songs.name AS song_name, songs.id AS song_id, artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN songs ON albumsongs.song_id = songs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN artists ON artists.id = albumsongsartists.artist_id
\\GROUP BY songs.id, artists.id
\\ORDER BY scrobbles DESC, songs.name ASC
,
.album =>
\\SELECT albums.name AS album_name, albums.id AS album_id, artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN albums ON albumsongs.album_id = albums.id
\\INNER JOIN scrobbles ON albumsongs.id = scrobbles.albumsong
\\INNER JOIN artistalbums ON artistalbums.album_id = albums.id
\\INNER JOIN artists ON artists.id = artistalbums.artist_id
\\GROUP BY albums.id, artists.id
\\ORDER BY scrobbles DESC
,
.artist =>
\\SELECT artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongsartists
\\INNER JOIN artists ON albumsongsartists.artist_id = artists.id
\\INNER JOIN albumsongs ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\GROUP BY artists.id
\\ORDER BY scrobbles DESC;
,
},
.appears =>
// Not sure how I feel about this one
switch (entity) {
.scrobble, .song, .album => unreachable,
.artist =>
\\SELECT albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM artistalbums
\\INNER JOIN albums ON albums.id = artistalbums.album_id
\\INNER JOIN albumsongs ON albumsongs.album_id = albums.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongsartists.artist_id = $1 AND artistalbums.artist_id != $1
\\GROUP BY albums.id
\\ORDER BY scrobbles DESC;
,
},
.get_songs => switch (entity) {
.scrobble, .song => unreachable, // Might be able to use this with SongGroups?
.album =>
\\SELECT songs.name AS song_name, songs.id AS song_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN songs ON albumsongs.song_id = songs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE albumsongs.album_id = $1
\\GROUP BY songs.id
\\ORDER BY scrobbles DESC
,
.artist =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE albumsongsartists.artist_id = $1
\\GROUP BY songs.id, albums.id
\\ORDER BY scrobbles DESC
,
},
.get_albums =>
//.ResultType = EntityItemsResult,
switch (entity) {
.scrobble, .album => unreachable, // Might be able to use this with ReleaseGroups?
.song =>
\\SELECT albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM artistalbums
\\INNER JOIN albums ON albums.id = artistalbums.album_id
\\INNER JOIN albumsongs ON albumsongs.album_id = albums.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = albumsongs.id
\\WHERE albumsongs.song_id = $1
\\GROUP BY albums.id
\\ORDER BY scrobbles DESC
,
.artist =>
\\SELECT albums.name AS album_name, albums.id AS album_id, COUNT(scrobbles) AS scrobbles
\\FROM artistalbums
\\INNER JOIN albums ON albums.id = artistalbums.album_id
\\INNER JOIN albumsongs ON albumsongs.album_id = albums.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE artistalbums.artist_id = $1
\\GROUP BY albums.id
\\ORDER BY scrobbles DESC
,
},
.get_scrobbles => switch (entity) {
.scrobble => unreachable,
.song =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE songs.id = $1
\\ORDER BY date ASC
,
.album =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE albums.id = $1
\\ORDER BY date ASC
,
.artist =>
\\SELECT songs.name AS song_name, songs.id AS song_id, albums.name AS album_name, albums.id AS album_id, TO_CHAR(scrobbles.datetime, 'YYYY-MM-DD HH24:MI:SS') AS date
\\FROM albumsongs
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\WHERE artists.id = $1
\\ORDER BY date ASC
,
},
.entity_info => switch (entity) {
.scrobble => unreachable,
.song =>
\\SELECT * FROM
\\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM99999th') AS rank FROM
\\(SELECT songs.name AS song_name, songs.id AS song_id, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN songs ON albumsongs.song_id = songs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\GROUP BY songs.id))
\\WHERE song_id = $1
,
.album =>
\\SELECT album_name, t.album_id, artists.name AS artist_name, artists.id AS artist_id, song_num, scrobbles, rank FROM
\\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM9999th') AS rank FROM
\\(SELECT albums.name AS album_name, albums.id AS album_id, COUNT(DISTINCT songs.id) AS song_num, COUNT(scrobbles) AS scrobbles
\\FROM albumsongs
\\INNER JOIN albums ON albumsongs.album_id = albums.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\GROUP BY albums.id)) AS t
\\INNER JOIN artistalbums ON artistalbums.album_id = t.album_id
\\INNER JOIN artists ON artists.id = artistalbums.artist_id
\\WHERE t.album_id = $1
,
.artist =>
\\SELECT * FROM
\\(SELECT *, TO_CHAR(ROW_NUMBER() OVER (ORDER BY scrobbles DESC),'FM9999th') AS rank FROM
\\(SELECT artists.name AS artist_name, artists.id AS artist_id, COUNT(scrobbles) AS scrobbles, COUNT(DISTINCT albums.id) AS album_num, COUNT(DISTINCT songs.id) AS song_num
\\FROM albumsongsartists
\\INNER JOIN artists ON albumsongsartists.artist_id = artists.id
\\INNER JOIN albumsongs ON albumsongsartists.albumsong_id = albumsongs.id
\\INNER JOIN scrobbles ON scrobbles.albumsong = albumsongs.id
\\INNER JOIN albums ON albums.id = albumsongs.album_id
\\INNER JOIN songs ON songs.id = albumsongs.song_id
\\GROUP BY artists.id))
\\WHERE artist_id = $1
,
},
.datestreak => switch (entity) {
.song =>
\\SELECT maxseq AS streak, FORMAT('%s - %s', ds, de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
\\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
\\COUNT(DISTINCT datetime::date) AS numdays
\\FROM (SELECT scrobbles.datetime,
\\((datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY songs.id ORDER BY datetime::date)) AS grp
\\FROM scrobbles INNER JOIN albumsongs ON albumsongs.id = albumsong INNER JOIN songs ON songs.id = albumsongs.song_id
\\WHERE songs.id = $1) scrobbles
\\GROUP BY grp
\\) scrobbles
\\GROUP BY ds, de)
\\ORDER BY maxseq DESC;
,
.artist =>
\\SELECT maxseq AS streak, FORMAT('%s - %s' ,ds,de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
\\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
\\COUNT(DISTINCT datetime::date) AS numdays
\\FROM (SELECT scrobbles.datetime,
\\((scrobbles.datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY artists.id ORDER BY scrobbles.datetime::date)) AS grp
\\FROM scrobbles INNER JOIN albumsongsartists ON albumsongsartists.albumsong_id = scrobbles.albumsong INNER JOIN artists ON artists.id = albumsongsartists.artist_id
\\WHERE artists.id = $1) scrobbles
\\GROUP BY grp
\\) scrobbles
\\GROUP BY ds, de)
\\ORDER BY maxseq DESC;
,
.album =>
\\SELECT maxseq AS streak, FORMAT('%s - %s', ds, de) AS date FROM (SELECT MAX(numdays) AS maxseq, ds, de
\\FROM (SELECT grp, MIN(datetime::date) AS ds, MAX(datetime::date) AS de,
\\COUNT(DISTINCT datetime::date) AS numdays
\\FROM (SELECT scrobbles.datetime,
\\((datetime::date - '1970-01-01'::date) - DENSE_RANK() OVER (PARTITION BY albums.id ORDER BY datetime::date)) AS grp FROM scrobbles INNER JOIN albumsongs ON albumsongs.id = albumsong INNER JOIN albums ON albums.id = albumsongs.album_id
\\WHERE albums.id = $1) scrobbles
\\GROUP BY grp
\\) scrobbles
\\GROUP BY ds, de)
\\ORDER BY maxseq DESC;
,
.scrobble => unreachable,
},
},
};
}