diff --git a/PluralKit.Core/Stores.cs b/PluralKit.Core/Stores.cs index b53e3872..81566f82 100644 --- a/PluralKit.Core/Stores.cs +++ b/PluralKit.Core/Stores.cs @@ -337,6 +337,48 @@ namespace PluralKit { return await conn.QueryAsync("select * from switches where system = @System order by timestamp desc limit @Count", new {System = system.Id, Count = count}); } + public struct SwitchMembersListEntry + { + public int Member; + public Instant Timestamp; + } + + public async Task> GetSwitchMembersList(PKSystem system, Instant start, Instant end) + { + // Wrap multiple commands in a single transaction for performance + using (var conn = await _conn.Obtain()) + using (var tx = conn.BeginTransaction()) + { + // Find the time of the last switch outside the range as it overlaps the range + // If no prior switch exists, the lower bound of the range remains the start time + var lastSwitch = await conn.QuerySingleOrDefaultAsync( + @"SELECT COALESCE(MAX(timestamp), @Start) + FROM switches + WHERE switches.system = @System + AND switches.timestamp < @Start", + new { System = system.Id, Start = start }); + + // Then collect the time and members of all switches that overlap the range + var switchMembersEntries = await conn.QueryAsync( + @"SELECT switch_members.member, switches.timestamp + FROM switches + JOIN switch_members + ON switches.id = switch_members.switch + WHERE switches.system = @System + AND ( + switches.timestamp >= @Start + OR switches.timestamp = @LastSwitch + ) + AND switches.timestamp < @End + ORDER BY switches.timestamp DESC", + new { System = system.Id, Start = start, End = end, LastSwitch = lastSwitch }); + + // Commit and return the list + tx.Commit(); + return switchMembersEntries; + } + } + public async Task> GetSwitchMemberIds(PKSwitch sw) { using (var conn = await _conn.Obtain()) @@ -386,14 +428,8 @@ namespace PluralKit { public async Task> GetTruncatedSwitchList(PKSystem system, Instant periodStart, Instant periodEnd) { - // TODO: only fetch the necessary switches here - // todo: this is in general not very efficient LOL - // returns switches in chronological (newest first) order - var switches = await GetSwitches(system); - - // we skip all switches that happened later than the range end, and taking all the ones that happened after the range start - // *BUT ALSO INCLUDING* the last switch *before* the range (that partially overlaps the range period) - var switchesInRange = switches.SkipWhile(sw => sw.Timestamp >= periodEnd).TakeWhileIncluding(sw => sw.Timestamp > periodStart).ToList(); + // Returns the timestamps and member IDs of switches overlapping the range, in chronological (newest first) order + var switchMembers = await GetSwitchMembersList(system, periodStart, periodEnd); // query DB for all members involved in any of the switches above and collect into a dictionary for future use // this makes sure the return list has the same instances of PKMember throughout, which is important for the dictionary @@ -401,34 +437,43 @@ namespace PluralKit { Dictionary memberObjects; using (var conn = await _conn.Obtain()) { - memberObjects = (await conn.QueryAsync( - "select distinct members.* from members, switch_members where switch_members.switch = any(@Switches) and switch_members.member = members.id", // lol postgres specific `= any()` syntax - new {Switches = switchesInRange.Select(sw => sw.Id).ToList()})) - .ToDictionary(m => m.Id); + memberObjects = ( + await conn.QueryAsync( + "select * from members where id = any(@Switches)", // lol postgres specific `= any()` syntax + new { Switches = switchMembers.Select(m => m.Member).Distinct().ToList() }) + ).ToDictionary(m => m.Id); } + // Initialize entries - still need to loop to determine the TimespanEnd below + var entries = + from item in switchMembers + group item by item.Timestamp into g + select new SwitchListEntry + { + TimespanStart = g.Key, + Members = g.Select(x => memberObjects[x.Member]).ToList() + }; - // we create the entry objects - var outList = new List(); - - // loop through every switch that *occurred* in-range and add it to the list - // end time is the switch *after*'s timestamp - we cheat and start it out at the range end so the first switch in-range "ends" there instead of the one after's start point + // Loop through every switch that overlaps the range and add it to the output list + // end time is the *FOLLOWING* switch's timestamp - we cheat by working backwards from the range end, so no dates need to be compared var endTime = periodEnd; - foreach (var switchInRange in switchesInRange) + var outList = new List(); + foreach (var e in entries) { - // find the start time of the switch, but clamp it to the range (only applicable to the Last Switch Before Range we include in the TakeWhileIncluding call above) - var switchStartClamped = switchInRange.Timestamp; - if (switchStartClamped < periodStart) switchStartClamped = periodStart; + // Override the start time of the switch if it's outside the range (only true for the "out of range" switch we included above) + var switchStartClamped = e.TimespanStart < periodStart + ? periodStart + : e.TimespanStart; outList.Add(new SwitchListEntry { - Members = (await GetSwitchMemberIds(switchInRange)).Select(id => memberObjects[id]).ToList(), + Members = e.Members, TimespanStart = switchStartClamped, TimespanEnd = endTime }); - // next switch's end is this switch's start - endTime = switchInRange.Timestamp; + // next switch's end is this switch's start (we're working backward in time) + endTime = e.TimespanStart; } return outList; diff --git a/PluralKit.Core/db_schema.sql b/PluralKit.Core/db_schema.sql index 3fe1848d..d0108cca 100644 --- a/PluralKit.Core/db_schema.sql +++ b/PluralKit.Core/db_schema.sql @@ -49,6 +49,10 @@ create table if not exists switches system serial not null references systems (id) on delete cascade, timestamp timestamp not null default (current_timestamp at time zone 'utc') ); +CREATE INDEX IF NOT EXISTS idx_switches_system +ON switches USING btree ( + system ASC NULLS LAST +) INCLUDE ("timestamp"); create table if not exists switch_members ( @@ -56,6 +60,10 @@ create table if not exists switch_members switch serial not null references switches (id) on delete cascade, member serial not null references members (id) on delete cascade ); +CREATE INDEX IF NOT EXISTS idx_switch_members_switch +ON switch_members USING btree ( + switch ASC NULLS LAST +) INCLUDE (member); create table if not exists webhooks (