Refactor sort/filter code once again
Now we handle sorting on the bot side, but still filter in the database
This commit is contained in:
parent
0bb8d2b917
commit
6d06474d26
@ -1,29 +1,18 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
using Dapper;
|
|
||||||
|
|
||||||
using NodaTime;
|
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace PluralKit.Bot
|
namespace PluralKit.Bot
|
||||||
{
|
{
|
||||||
public class SystemList
|
public class SystemList
|
||||||
{
|
{
|
||||||
private readonly IClock _clock;
|
|
||||||
private readonly IDatabase _db;
|
private readonly IDatabase _db;
|
||||||
private readonly ILogger _logger;
|
|
||||||
|
|
||||||
public SystemList(IDatabase db, ILogger logger, IClock clock)
|
public SystemList(IDatabase db)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_logger = logger;
|
|
||||||
_clock = clock;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task MemberList(Context ctx, PKSystem target)
|
public async Task MemberList(Context ctx, PKSystem target)
|
||||||
@ -34,7 +23,8 @@ namespace PluralKit.Bot
|
|||||||
// GetRendererFor must be called before GetOptions as it consumes a potential positional full argument that'd otherwise land in the filter
|
// GetRendererFor must be called before GetOptions as it consumes a potential positional full argument that'd otherwise land in the filter
|
||||||
var renderer = GetRendererFor(ctx);
|
var renderer = GetRendererFor(ctx);
|
||||||
var opts = GetOptions(ctx, target);
|
var opts = GetOptions(ctx, target);
|
||||||
var members = await GetMemberList(target, opts);
|
|
||||||
|
var members = (await _db.Execute(c => opts.Execute(c, target))).ToList();
|
||||||
await ctx.Paginate(
|
await ctx.Paginate(
|
||||||
members.ToAsyncEnumerable(),
|
members.ToAsyncEnumerable(),
|
||||||
members.Count,
|
members.Count,
|
||||||
@ -43,27 +33,11 @@ namespace PluralKit.Bot
|
|||||||
(eb, ms) =>
|
(eb, ms) =>
|
||||||
{
|
{
|
||||||
eb.WithFooter($"{opts.CreateFilterString()}. {members.Count} results.");
|
eb.WithFooter($"{opts.CreateFilterString()}. {members.Count} results.");
|
||||||
renderer.RenderPage(eb, ctx.System, ms);
|
renderer.RenderPage(eb, ctx.System.Zone, ms);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyList<PKListMember>> GetMemberList(PKSystem target, SortFilterOptions opts)
|
|
||||||
{
|
|
||||||
await using var conn = await _db.Obtain();
|
|
||||||
var query = opts.BuildQuery();
|
|
||||||
var args = new {System = target.Id, opts.Filter};
|
|
||||||
_logger.Debug("Executing sort/filter query `{Query}` with arguments {Args}", query, args);
|
|
||||||
|
|
||||||
var timeBefore = _clock.GetCurrentInstant();
|
|
||||||
var results = (await conn.QueryAsync<PKListMember>(query, args)).ToList();
|
|
||||||
var timeAfter = _clock.GetCurrentInstant();
|
|
||||||
|
|
||||||
_logger.Debug("Executed sort/filter query `{Query}` returning {ResultCount} results in {QueryTime}", query, results.Count, timeAfter - timeBefore);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetEmbedTitle(PKSystem target, SortFilterOptions opts)
|
private string GetEmbedTitle(PKSystem target, SortFilterOptions opts)
|
||||||
{
|
{
|
||||||
var title = new StringBuilder("Members of ");
|
var title = new StringBuilder("Members of ");
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
||||||
namespace PluralKit.Bot
|
namespace PluralKit.Bot
|
||||||
@ -9,6 +11,6 @@ namespace PluralKit.Bot
|
|||||||
public interface IListRenderer
|
public interface IListRenderer
|
||||||
{
|
{
|
||||||
int MembersPerPage { get; }
|
int MembersPerPage { get; }
|
||||||
void RenderPage(DiscordEmbedBuilder eb, PKSystem system, IEnumerable<PKListMember> members);
|
void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
|
|
||||||
@ -21,8 +20,10 @@ namespace PluralKit.Bot
|
|||||||
_fields = fields;
|
_fields = fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RenderPage(DiscordEmbedBuilder eb, PKSystem system, IEnumerable<PKListMember> members)
|
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone zone, IEnumerable<ListedMember> members)
|
||||||
{
|
{
|
||||||
|
string FormatTimestamp(Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(zone));
|
||||||
|
|
||||||
foreach (var m in members)
|
foreach (var m in members)
|
||||||
{
|
{
|
||||||
var profile = $"**ID**: {m.Hid}";
|
var profile = $"**ID**: {m.Hid}";
|
||||||
@ -31,8 +32,8 @@ namespace PluralKit.Bot
|
|||||||
if (_fields.ShowBirthday && m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
if (_fields.ShowBirthday && m.Birthday != null) profile += $"\n**Birthdate**: {m.BirthdayString}";
|
||||||
if (_fields.ShowPronouns && m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
|
if (_fields.ShowPronouns && m.ProxyTags.Count > 0) profile += $"\n**Proxy tags:** {m.ProxyTagsString()}";
|
||||||
if (_fields.ShowMessageCount && m.MessageCount > 0) profile += $"\n**Message count:** {m.MessageCount}";
|
if (_fields.ShowMessageCount && m.MessageCount > 0) profile += $"\n**Message count:** {m.MessageCount}";
|
||||||
if (_fields.ShowLastMessage && m.LastMessage != null) profile += $"\n**Last message:** {FormatTimestamp(system, DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))}";
|
if (_fields.ShowLastMessage && m.LastMessage != null) profile += $"\n**Last message:** {FormatTimestamp(DiscordUtils.SnowflakeToInstant(m.LastMessage.Value))}";
|
||||||
if (_fields.ShowLastSwitch && m.LastSwitchTime != null) profile += $"\n**Last switched in:** {FormatTimestamp(system, m.LastSwitchTime.Value)}";
|
if (_fields.ShowLastSwitch && m.LastSwitchTime != null) profile += $"\n**Last switched in:** {FormatTimestamp(m.LastSwitchTime.Value)}";
|
||||||
if (_fields.ShowDescription && m.Description != null) profile += $"\n\n{m.Description}";
|
if (_fields.ShowDescription && m.Description != null) profile += $"\n\n{m.Description}";
|
||||||
if (_fields.ShowPrivacy && m.MemberPrivacy == PrivacyLevel.Private)
|
if (_fields.ShowPrivacy && m.MemberPrivacy == PrivacyLevel.Private)
|
||||||
profile += "\n*(this member is private)*";
|
profile += "\n*(this member is private)*";
|
||||||
@ -40,9 +41,7 @@ namespace PluralKit.Bot
|
|||||||
eb.AddField(m.Name, profile.Truncate(1024));
|
eb.AddField(m.Name, profile.Truncate(1024));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatTimestamp(PKSystem system, Instant timestamp) => DateTimeFormats.ZonedDateTimeFormat.Format(timestamp.InZone(system.Zone ?? DateTimeZone.Utc));
|
|
||||||
|
|
||||||
public class MemberFields
|
public class MemberFields
|
||||||
{
|
{
|
||||||
public bool ShowDisplayName = true;
|
public bool ShowDisplayName = true;
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
using NodaTime;
|
|
||||||
|
|
||||||
using PluralKit.Core;
|
|
||||||
|
|
||||||
namespace PluralKit.Bot
|
|
||||||
{
|
|
||||||
public class PKListMember: PKMember
|
|
||||||
{
|
|
||||||
public ulong? LastMessage { get; set; }
|
|
||||||
public Instant? LastSwitchTime { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,8 @@ using System.Text;
|
|||||||
|
|
||||||
using DSharpPlus.Entities;
|
using DSharpPlus.Entities;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
||||||
namespace PluralKit.Bot
|
namespace PluralKit.Bot
|
||||||
@ -11,9 +13,9 @@ namespace PluralKit.Bot
|
|||||||
{
|
{
|
||||||
public int MembersPerPage => 25;
|
public int MembersPerPage => 25;
|
||||||
|
|
||||||
public void RenderPage(DiscordEmbedBuilder eb, PKSystem system, IEnumerable<PKListMember> members)
|
public void RenderPage(DiscordEmbedBuilder eb, DateTimeZone timezone, IEnumerable<ListedMember> members)
|
||||||
{
|
{
|
||||||
string RenderLine(PKListMember m)
|
string RenderLine(ListedMember m)
|
||||||
{
|
{
|
||||||
if (m.HasProxyTags)
|
if (m.HasProxyTags)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
using PluralKit.Core;
|
using PluralKit.Core;
|
||||||
|
|
||||||
@ -15,7 +21,7 @@ namespace PluralKit.Bot
|
|||||||
|
|
||||||
public string CreateFilterString()
|
public string CreateFilterString()
|
||||||
{
|
{
|
||||||
StringBuilder str = new StringBuilder();
|
var str = new StringBuilder();
|
||||||
str.Append("Sorting by ");
|
str.Append("Sorting by ");
|
||||||
str.Append(SortProperty switch
|
str.Append(SortProperty switch
|
||||||
{
|
{
|
||||||
@ -46,69 +52,53 @@ namespace PluralKit.Bot
|
|||||||
|
|
||||||
return str.ToString();
|
return str.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string BuildQuery()
|
|
||||||
{
|
|
||||||
// For best performance, add index:
|
|
||||||
// - `on switch_members using btree (member asc nulls last) include (switch);`
|
|
||||||
// TODO: add a migration adding this, perhaps lumped with the rest of the DB changes (it's there in prod)
|
|
||||||
|
|
||||||
// Select clause
|
|
||||||
StringBuilder query = new StringBuilder("select * from member_list");
|
|
||||||
|
|
||||||
// Filtering
|
|
||||||
query.Append(" where system = @System");
|
|
||||||
query.Append(PrivacyFilter switch
|
|
||||||
{
|
|
||||||
PrivacyFilter.PrivateOnly => $" and member_privacy = {(int) PrivacyLevel.Private}",
|
|
||||||
PrivacyFilter.PublicOnly => $" and member_privacy = {(int) PrivacyLevel.Public}",
|
|
||||||
_ => ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// String filter
|
|
||||||
if (Filter != null)
|
|
||||||
{
|
|
||||||
// Use position rather than ilike to not bother with escaping and such
|
|
||||||
query.Append(" and (");
|
|
||||||
query.Append(
|
|
||||||
"position(lower(@Filter) in lower(name)) > 0 or position(lower(@Filter) in lower(coalesce(display_name, ''))) > 0");
|
|
||||||
if (SearchInDescription)
|
|
||||||
query.Append(" or position(lower(@Filter) in lower(coalesce(description, ''))) > 0");
|
|
||||||
query.Append(")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Order direction
|
|
||||||
var direction = SortProperty switch
|
|
||||||
{
|
|
||||||
// Some of these make more "logical sense" as descending (ie. "last message" = descending order of message timestamp/ID)
|
|
||||||
SortProperty.MessageCount => SortDirection.Descending,
|
|
||||||
SortProperty.LastMessage => SortDirection.Descending,
|
|
||||||
SortProperty.LastSwitch => SortDirection.Descending,
|
|
||||||
_ => SortDirection.Ascending
|
|
||||||
};
|
|
||||||
if (Reverse) direction = direction == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending;
|
|
||||||
var order = direction == SortDirection.Ascending ? "asc" : "desc";
|
|
||||||
|
|
||||||
// Order clause
|
|
||||||
const string fallback = "name collate \"C\" asc"; // how to handle null values
|
|
||||||
query.Append(" order by ");
|
|
||||||
query.Append(SortProperty switch
|
|
||||||
{
|
|
||||||
// Name/DN order needs `collate "C"` to match legacy .NET behavior (affects sorting of emojis, etc)
|
|
||||||
SortProperty.Name => $"name collate \"C\" {order}",
|
|
||||||
SortProperty.DisplayName => $"display_name collate \"C\" {order}, name collate \"C\" {order}",
|
|
||||||
SortProperty.Hid => $"hid {order}",
|
|
||||||
SortProperty.CreationDate => $"created {order}",
|
|
||||||
SortProperty.Birthdate => $"birthday_md {order} nulls last, {fallback}",
|
|
||||||
SortProperty.MessageCount => $"message_count {order} nulls last, {fallback}",
|
|
||||||
SortProperty.LastMessage => $"last_message {order} nulls last, {fallback}",
|
|
||||||
SortProperty.LastSwitch => $"last_switch_time {order} nulls last, {fallback}",
|
|
||||||
_ => throw new ArgumentOutOfRangeException($"Couldn't find order clause for sort property {SortProperty}")
|
|
||||||
});
|
|
||||||
|
|
||||||
return query.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public async Task<IEnumerable<ListedMember>> Execute(IPKConnection conn, PKSystem system)
|
||||||
|
{
|
||||||
|
var filtered = await QueryWithFilter(conn, system);
|
||||||
|
return Sort(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<IEnumerable<ListedMember>> QueryWithFilter(IPKConnection conn, PKSystem system) =>
|
||||||
|
conn.QueryMemberList(system.Id, PrivacyFilter switch
|
||||||
|
{
|
||||||
|
PrivacyFilter.PrivateOnly => PrivacyLevel.Private,
|
||||||
|
PrivacyFilter.PublicOnly => PrivacyLevel.Public,
|
||||||
|
PrivacyFilter.All => null
|
||||||
|
}, Filter, SearchInDescription);
|
||||||
|
|
||||||
|
private IEnumerable<ListedMember> Sort(IEnumerable<ListedMember> input)
|
||||||
|
{
|
||||||
|
IComparer<T> ReverseMaybe<T>(IComparer<T> c) =>
|
||||||
|
Reverse ? Comparer<T>.Create((a, b) => c.Compare(b, a)) : c;
|
||||||
|
|
||||||
|
var culture = StringComparer.InvariantCultureIgnoreCase;
|
||||||
|
return (SortProperty switch
|
||||||
|
{
|
||||||
|
// As for the OrderByDescending HasValue calls: https://www.jerriepelser.com/blog/orderby-with-null-values/
|
||||||
|
// We want nulls last no matter what, even if orders are reversed
|
||||||
|
SortProperty.Hid => input.OrderBy(m => m.Hid, ReverseMaybe(culture)),
|
||||||
|
SortProperty.Name => input.OrderBy(m => m.Name, ReverseMaybe(culture)),
|
||||||
|
SortProperty.CreationDate => input.OrderBy(m => m.Created, ReverseMaybe(Comparer<Instant>.Default)),
|
||||||
|
SortProperty.MessageCount => input.OrderByDescending(m => m.MessageCount, ReverseMaybe(Comparer<int>.Default)),
|
||||||
|
SortProperty.DisplayName => input
|
||||||
|
.OrderByDescending(m => m.DisplayName != null)
|
||||||
|
.ThenBy(m => m.DisplayName, ReverseMaybe(culture)),
|
||||||
|
SortProperty.Birthdate => input
|
||||||
|
.OrderByDescending(m => m.AnnualBirthday.HasValue)
|
||||||
|
.ThenBy(m => m.AnnualBirthday, ReverseMaybe(Comparer<AnnualDate?>.Default)),
|
||||||
|
SortProperty.LastMessage => input
|
||||||
|
.OrderByDescending(m => m.LastMessage.HasValue)
|
||||||
|
.ThenByDescending(m => m.LastMessage, ReverseMaybe(Comparer<ulong?>.Default)),
|
||||||
|
SortProperty.LastSwitch => input
|
||||||
|
.OrderByDescending(m => m.LastSwitchTime.HasValue)
|
||||||
|
.ThenByDescending(m => m.LastSwitchTime, ReverseMaybe(Comparer<Instant?>.Default)),
|
||||||
|
_ => throw new ArgumentOutOfRangeException($"Unknown sort property {SortProperty}")
|
||||||
|
})
|
||||||
|
// Lastly, add a by-name fallback order for collisions (generally hits w/ lots of null values)
|
||||||
|
.ThenBy(m => m.Name, culture);
|
||||||
|
}
|
||||||
|
|
||||||
public static SortFilterOptions FromFlags(Context ctx)
|
public static SortFilterOptions FromFlags(Context ctx)
|
||||||
{
|
{
|
||||||
var p = new SortFilterOptions();
|
var p = new SortFilterOptions();
|
||||||
@ -138,7 +128,6 @@ namespace PluralKit.Bot
|
|||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortProperty
|
public enum SortProperty
|
||||||
@ -153,12 +142,6 @@ namespace PluralKit.Bot
|
|||||||
Birthdate
|
Birthdate
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SortDirection
|
|
||||||
{
|
|
||||||
Ascending,
|
|
||||||
Descending
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum PrivacyFilter
|
public enum PrivacyFilter
|
||||||
{
|
{
|
||||||
All,
|
All,
|
||||||
|
@ -82,7 +82,7 @@ namespace PluralKit.Core
|
|||||||
await ApplyMigrations(conn, tx);
|
await ApplyMigrations(conn, tx);
|
||||||
|
|
||||||
// Now, reapply views/functions (we deleted them above, no need to worry about conflicts)
|
// Now, reapply views/functions (we deleted them above, no need to worry about conflicts)
|
||||||
await ExecuteSqlFile($"{RootPath}.views.sql", conn, tx);
|
await ExecuteSqlFile($"{RootPath}.Views.views.sql", conn, tx);
|
||||||
await ExecuteSqlFile($"{RootPath}.Functions.functions.sql", conn, tx);
|
await ExecuteSqlFile($"{RootPath}.Functions.functions.sql", conn, tx);
|
||||||
|
|
||||||
// Finally, commit tx
|
// Finally, commit tx
|
||||||
|
34
PluralKit.Core/Database/Views/DatabaseViewsExt.cs
Normal file
34
PluralKit.Core/Database/Views/DatabaseViewsExt.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
#nullable enable
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace PluralKit.Core
|
||||||
|
{
|
||||||
|
public static class DatabaseViewsExt
|
||||||
|
{
|
||||||
|
public static Task<IEnumerable<SystemFronter>> QueryCurrentFronters(this IPKConnection conn, int system) =>
|
||||||
|
conn.QueryAsync<SystemFronter>("select * from system_fronters where system = @system", new {system});
|
||||||
|
|
||||||
|
public static Task<IEnumerable<ListedMember>> QueryMemberList(this IPKConnection conn, int system, PrivacyLevel? privacyFilter = null, string? filter = null, bool includeDescriptionInNameFilter = false)
|
||||||
|
{
|
||||||
|
StringBuilder query = new StringBuilder("select * from member_list where system = @system");
|
||||||
|
|
||||||
|
if (privacyFilter != null)
|
||||||
|
query.Append($" and member_privacy = {(int) privacyFilter}");
|
||||||
|
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
static string Filter(string column) => $"position(lower(@filter) in lower(coalesce({column}, ''))) > 0";
|
||||||
|
|
||||||
|
query.Append($" and ({Filter("name")} or {Filter("display_name")}");
|
||||||
|
if (includeDescriptionInNameFilter) query.Append($" or {Filter("description")}");
|
||||||
|
query.Append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn.QueryAsync<ListedMember>(query.ToString(), new {system, filter});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
PluralKit.Core/Database/Views/ListedMember.cs
Normal file
17
PluralKit.Core/Database/Views/ListedMember.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#nullable enable
|
||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.Core
|
||||||
|
{
|
||||||
|
// TODO: is inheritance here correct?
|
||||||
|
public class ListedMember: PKMember
|
||||||
|
{
|
||||||
|
public ulong? LastMessage { get; }
|
||||||
|
public Instant? LastSwitchTime { get; }
|
||||||
|
|
||||||
|
public AnnualDate? AnnualBirthday =>
|
||||||
|
Birthday != null
|
||||||
|
? new AnnualDate(Birthday.Value.Month, Birthday.Value.Day)
|
||||||
|
: (AnnualDate?) null;
|
||||||
|
}
|
||||||
|
}
|
14
PluralKit.Core/Database/Views/SystemFronter.cs
Normal file
14
PluralKit.Core/Database/Views/SystemFronter.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using NodaTime;
|
||||||
|
|
||||||
|
namespace PluralKit.Core
|
||||||
|
{
|
||||||
|
public class SystemFronter
|
||||||
|
{
|
||||||
|
public int SystemId { get; }
|
||||||
|
public int SwitchId { get; }
|
||||||
|
public Instant SwitchTimestamp { get; }
|
||||||
|
public int MemberId { get; }
|
||||||
|
public string MemberHid { get; }
|
||||||
|
public string MemberName { get; }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
create view system_last_switch as
|
-- Returns one row per system, containing info about latest switch + array of member IDs (for future joins)
|
||||||
|
create view system_last_switch as
|
||||||
select systems.id as system,
|
select systems.id as system,
|
||||||
last_switch.id as switch,
|
last_switch.id as switch,
|
||||||
last_switch.timestamp as timestamp,
|
last_switch.timestamp as timestamp,
|
||||||
@ -6,6 +7,25 @@ select systems.id as system,
|
|||||||
from systems
|
from systems
|
||||||
inner join lateral (select * from switches where switches.system = systems.id order by timestamp desc limit 1) as last_switch on true;
|
inner join lateral (select * from switches where switches.system = systems.id order by timestamp desc limit 1) as last_switch on true;
|
||||||
|
|
||||||
|
-- Returns one row for every current fronter in a system, w/ some member info
|
||||||
|
create view system_fronters as
|
||||||
|
select
|
||||||
|
systems.id as system_id,
|
||||||
|
last_switch.id as switch_id,
|
||||||
|
last_switch.timestamp as switch_timestamp,
|
||||||
|
members.id as member_id,
|
||||||
|
members.hid as member_hid,
|
||||||
|
members.name as member_name
|
||||||
|
from systems
|
||||||
|
-- TODO: is there a more efficient way of doing this search? might need to index on timestamp if we haven't in prod
|
||||||
|
inner join lateral (select * from switches where switches.system = systems.id order by timestamp desc limit 1) as last_switch on true
|
||||||
|
|
||||||
|
-- change to left join to handle memberless switches?
|
||||||
|
inner join switch_members on switch_members.switch = last_switch.system
|
||||||
|
inner join members on members.id = switch_members.member
|
||||||
|
-- return them in order of the switch itself
|
||||||
|
order by switch_members.id;
|
||||||
|
|
||||||
create view member_list as
|
create view member_list as
|
||||||
select members.*,
|
select members.*,
|
||||||
-- Find last message ID
|
-- Find last message ID
|
@ -1,4 +1,9 @@
|
|||||||
drop view if exists system_last_switch;
|
-- This gets run on every bot startup and makes sure we're starting from a clean slate
|
||||||
|
-- Then, the views/functions.sql files get run, and they recreate the necessary objects
|
||||||
|
-- This does mean we can't use any functions in row triggers, etc. Still unsure how to handle this.
|
||||||
|
|
||||||
|
drop view if exists system_last_switch;
|
||||||
|
drop view if exists system_fronters;
|
||||||
drop view if exists member_list;
|
drop view if exists member_list;
|
||||||
|
|
||||||
drop function if exists message_context;
|
drop function if exists message_context;
|
||||||
|
Loading…
Reference in New Issue
Block a user