Merge pull request #37 from NoiTheCat/sql-pool-experimenting

* Update to Entity Framework Core 7
* Switch to bulk updates/removals in DataRetention
* Remove connection pool configuration settings (including limits)
* Greatly reduce number of open connections existing at one time
This commit is contained in:
Noi 2022-11-22 23:32:39 -08:00 committed by GitHub
commit c0b94dc614
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 446 additions and 104 deletions

View file

@ -222,10 +222,10 @@ public class BirthdayModule : BotModuleBase {
private static List<ListItem> GetSortedUserList(SocketGuild guild) {
using var db = new BotDatabaseContext();
var query = from row in db.UserEntries
where row.GuildId == (long)guild.Id
where row.GuildId == guild.Id
orderby row.BirthMonth, row.BirthDay
select new {
UserId = (ulong)row.UserId,
row.UserId,
Month = row.BirthMonth,
Day = row.BirthDay,
Zone = row.TimeZone

View file

@ -56,7 +56,7 @@ public class ConfigModule : BotModuleBase {
[SlashCommand("set-channel", HelpPfxModOnly + HelpSubCmdChannel + HelpPofxBlankUnset)]
public async Task CmdSetChannel([Summary(description: HelpOptChannel)] SocketTextChannel? channel = null) {
await DoDatabaseUpdate(Context, s => s.AnnouncementChannel = (long?)channel?.Id);
await DoDatabaseUpdate(Context, s => s.AnnouncementChannel = channel?.Id);
await RespondAsync(":white_check_mark: The announcement channel has been " +
(channel == null ? "unset." : $"set to **{channel.Name}**."));
}
@ -126,7 +126,7 @@ public class ConfigModule : BotModuleBase {
await RespondAsync(":x: This role cannot be used for this setting.", ephemeral: true);
return;
}
await DoDatabaseUpdate(Context, s => s.BirthdayRole = (long)role.Id);
await DoDatabaseUpdate(Context, s => s.BirthdayRole = role.Id);
await RespondAsync($":white_check_mark: The birthday role has been set to **{role.Name}**.").ConfigureAwait(false);
}
@ -136,7 +136,7 @@ public class ConfigModule : BotModuleBase {
await RespondAsync(":x: This role cannot be used for this setting.", ephemeral: true);
return;
}
await DoDatabaseUpdate(Context, s => s.ModeratorRole = (long?)role?.Id);
await DoDatabaseUpdate(Context, s => s.ModeratorRole = role?.Id);
await RespondAsync(":white_check_mark: The moderator role has been " +
(role == null ? "unset." : $"set to **{role.Name}**."));
}
@ -154,7 +154,7 @@ public class ConfigModule : BotModuleBase {
// setting: true to add (set), false to remove (unset)
using var db = new BotDatabaseContext();
var existing = db.BlocklistEntries
.Where(bl => bl.GuildId == (long)user.Guild.Id && bl.UserId == (long)user.Id).FirstOrDefault();
.Where(bl => bl.GuildId == user.Guild.Id && bl.UserId == user.Id).FirstOrDefault();
bool already = (existing != null) == setting;
if (already) {
@ -162,7 +162,7 @@ public class ConfigModule : BotModuleBase {
return;
}
if (setting) db.BlocklistEntries.Add(new BlocklistEntry() { GuildId = (long)user.Guild.Id, UserId = (long)user.Id });
if (setting) db.BlocklistEntries.Add(new BlocklistEntry() { GuildId = user.Guild.Id, UserId = user.Id });
else db.Remove(existing!);
await db.SaveChangesAsync();

View file

@ -24,8 +24,8 @@ class EnforceBlockingAttribute : PreconditionAttribute {
using var db = new BotDatabaseContext();
var settings = (from row in db.GuildConfigurations
where row.GuildId == (long)guild.Id
select new { ModRole = (ulong?)row.ModeratorRole, ModMode = row.Moderated }).FirstOrDefault();
where row.GuildId == guild.Id
select new { ModRole = row.ModeratorRole, ModMode = row.Moderated }).FirstOrDefault();
if (settings != null) {
// Bot moderators override all blocking measures in place
if (user.Roles.Any(r => r.Id == settings.ModRole)) return Task.FromResult(PreconditionResult.FromSuccess());
@ -34,7 +34,7 @@ class EnforceBlockingAttribute : PreconditionAttribute {
if (settings.ModMode) return Task.FromResult(PreconditionResult.FromError(FailModerated));
// Check if user exists in blocklist
if (db.BlocklistEntries.Where(row => row.GuildId == (long)guild.Id && row.UserId == (long)user.Id).Any())
if (db.BlocklistEntries.Where(row => row.GuildId == guild.Id && row.UserId == user.Id).Any())
return Task.FromResult(PreconditionResult.FromError(FailBlocked));
}

View file

@ -22,7 +22,7 @@ class RequireBotModeratorAttribute : PreconditionAttribute {
if (user.GuildPermissions.ManageGuild) return Task.FromResult(PreconditionResult.FromSuccess());
using var db = new BotDatabaseContext();
var checkRole = (ulong?)db.GuildConfigurations
.Where(g => g.GuildId == (long)((SocketGuild)context.Guild).Id)
.Where(g => g.GuildId == ((SocketGuild)context.Guild).Id)
.Select(g => g.ModeratorRole).FirstOrDefault();
if (checkRole.HasValue && user.Roles.Any(r => r.Id == checkRole.Value))
return Task.FromResult(PreconditionResult.FromSuccess());

View file

@ -1,7 +1,7 @@
using BirthdayBot.Data;
using Microsoft.EntityFrameworkCore;
namespace BirthdayBot.BackgroundServices;
/// <summary>
/// Proactively fills the user cache for guilds in which any birthday data already exists.
/// </summary>
@ -9,14 +9,23 @@ class AutoUserDownload : BackgroundService {
public AutoUserDownload(ShardInstance instance) : base(instance) { }
public override async Task OnTick(int tickCount, CancellationToken token) {
using var db = new BotDatabaseContext();
// Take action if a guild's cache is incomplete...
var incompleteCaches = ShardInstance.DiscordClient.Guilds.Where(g => !g.HasAllMembers).Select(g => (long)g.Id).ToHashSet();
var incompleteCaches = ShardInstance.DiscordClient.Guilds.Where(g => !g.HasAllMembers).Select(g => g.Id).ToHashSet();
// ...and if the guild contains any user data
var mustFetch = db.UserEntries.Where(e => incompleteCaches.Contains(e.GuildId)).Select(e => e.GuildId).Distinct();
int processed = 0;
IEnumerable<ulong> mustFetch;
try {
await DbConcurrentOperationsLock.WaitAsync(token);
using var db = new BotDatabaseContext();
mustFetch = db.UserEntries.AsNoTracking()
.Where(e => incompleteCaches.Contains(e.GuildId)).Select(e => e.GuildId).Distinct()
.ToList();
} finally {
try {
DbConcurrentOperationsLock.Release();
} catch (ObjectDisposedException) { }
}
var processed = 0;
foreach (var item in mustFetch) {
// May cause a disconnect in certain situations. Cancel all further attempts until the next pass if it happens.
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) break;
@ -28,6 +37,6 @@ class AutoUserDownload : BackgroundService {
processed++;
}
if (processed > 100) Log($"Explicit user list request processed for {processed} guild(s).");
if (processed > 25) Log($"Explicit user list request processed for {processed} guild(s).");
}
}

View file

@ -27,9 +27,9 @@ class BirthdayRoleUpdate : BackgroundService {
private async Task ProcessBirthdaysAsync(CancellationToken token) {
// For database efficiency, fetch all database information at once before proceeding
using var db = new BotDatabaseContext();
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => g.Id).ToHashSet();
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create((ulong)s.GuildId, s));
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create(s.GuildId, s));
var exceptions = new List<Exception>();
foreach (var (guildId, settings) in guildChecks) {

View file

@ -1,4 +1,5 @@
using BirthdayBot.Data;
using Microsoft.EntityFrameworkCore;
using System.Text;
namespace BirthdayBot.BackgroundServices;
@ -31,34 +32,32 @@ class DataRetention : BackgroundService {
private async Task RemoveStaleEntriesAsync() {
using var db = new BotDatabaseContext();
var now = DateTimeOffset.UtcNow;
int updatedGuilds = 0, updatedUsers = 0;
// Update guilds
var localGuilds = ShardInstance.DiscordClient.Guilds.Select(g => g.Id).ToList();
var updatedGuilds = await db.GuildConfigurations
.Where(g => localGuilds.Contains(g.GuildId))
.ExecuteUpdateAsync(upd => upd.SetProperty(p => p.LastSeen, now));
// Update guild users
var updatedUsers = 0;
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
// Update guild, fetch users from database
var dbGuild = db.GuildConfigurations.Where(s => s.GuildId == (long)guild.Id).FirstOrDefault();
if (dbGuild == null) continue;
dbGuild.LastSeen = now;
updatedGuilds++;
// Update users
var localIds = guild.Users.Select(u => (long)u.Id);
var dbSavedIds = db.UserEntries.Where(e => e.GuildId == (long)guild.Id).Select(e => e.UserId);
var usersToUpdate = localIds.Intersect(dbSavedIds).ToHashSet();
foreach (var user in db.UserEntries.Where(e => e.GuildId == (long)guild.Id && usersToUpdate.Contains(e.UserId))) {
user.LastSeen = now;
updatedUsers++;
}
var localUsers = guild.Users.Select(u => u.Id).ToList();
updatedUsers += await db.UserEntries
.Where(gu => gu.GuildId == guild.Id)
.Where(gu => localUsers.Contains(gu.UserId))
.ExecuteUpdateAsync(upd => upd.SetProperty(p => p.LastSeen, now));
}
// And let go of old data
var staleGuilds = db.GuildConfigurations.Where(s => now - TimeSpan.FromDays(StaleGuildThreshold) > s.LastSeen);
var staleUsers = db.UserEntries.Where(e => now - TimeSpan.FromDays(StaleUserThreashold) > e.LastSeen);
int staleGuildCount = staleGuilds.Count(), staleUserCount = staleUsers.Count();
db.GuildConfigurations.RemoveRange(staleGuilds);
db.UserEntries.RemoveRange(staleUsers);
await db.SaveChangesAsync(CancellationToken.None);
var staleGuildCount = await db.GuildConfigurations
.Where(g => now - TimeSpan.FromDays(StaleGuildThreshold) > g.LastSeen)
.ExecuteDeleteAsync();
var staleUserCount = await db.UserEntries
.Where(gu => now - TimeSpan.FromDays(StaleUserThreashold) > gu.LastSeen)
.ExecuteDeleteAsync();
// Build report
var resultText = new StringBuilder();
resultText.Append($"Updated {updatedGuilds} guilds, {updatedUsers} users.");
if (staleGuildCount != 0 || staleUserCount != 0) {

View file

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>3.4.5</Version>
<Version>3.4.6</Version>
<Authors>NoiTheCat</Authors>
</PropertyGroup>
@ -23,16 +23,16 @@
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="Discord.Net" Version="3.8.1" />
<PackageReference Include="EFCore.NamingConventions" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.9">
<PackageReference Include="EFCore.NamingConventions" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.1.3" />
<PackageReference Include="Npgsql" Version="6.0.6" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="6.0.6" />
<PackageReference Include="Npgsql" Version="7.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.Design" Version="1.1.0" />
</ItemGroup>

View file

@ -73,7 +73,7 @@ class Configuration {
SqlDatabase = ReadConfKey<string?>(jc, nameof(SqlDatabase), false);
SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
SqlApplicationName = $"ClientShard{ShardStart}+{ShardAmount}";
SqlApplicationName = $"Shard{ShardStart:00}-{ShardStart + ShardAmount - 1:00}";
}
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {

View file

@ -6,11 +6,9 @@ namespace BirthdayBot.Data;
[Table("banned_users")]
public class BlocklistEntry {
[Key]
[Column("guild_id")]
public long GuildId { get; set; }
public ulong GuildId { get; set; }
[Key]
[Column("user_id")]
public long UserId { get; set; }
public ulong UserId { get; set; }
[ForeignKey(nameof(GuildConfig.GuildId))]
[InverseProperty(nameof(GuildConfig.BlockedUsers))]

View file

@ -14,8 +14,7 @@ public class BotDatabaseContext : DbContext {
Database = conf.SqlDatabase,
Username = conf.SqlUsername,
Password = conf.SqlPassword,
ApplicationName = conf.SqlApplicationName,
MaxPoolSize = Math.Max((int)Math.Ceiling(conf.ShardAmount * 2 * 0.6), 8)
ApplicationName = conf.SqlApplicationName
}.ToString();
}

View file

@ -6,14 +6,14 @@ internal static class Extensions {
/// If it doesn't exist in the database, <see cref="GuildConfig.IsNew"/> returns true.
/// </summary>
public static GuildConfig GetConfigOrNew(this SocketGuild guild, BotDatabaseContext db)
=> db.GuildConfigurations.Where(g => g.GuildId == (long)guild.Id).FirstOrDefault()
?? new GuildConfig() { IsNew = true, GuildId = (long)guild.Id };
=> db.GuildConfigurations.Where(g => g.GuildId == guild.Id).FirstOrDefault()
?? new GuildConfig() { IsNew = true, GuildId = guild.Id };
/// <summary>
/// Gets the corresponding <see cref="UserEntry"/> for this user in this guild, or a new one if one does not exist.
/// If it doesn't exist in the database, <see cref="UserEntry.IsNew"/> returns true.
/// </summary>
public static UserEntry GetUserEntryOrNew(this SocketGuildUser user, BotDatabaseContext db)
=> db.UserEntries.Where(u => u.GuildId == (long)user.Guild.Id && u.UserId == (long)user.Id).FirstOrDefault()
?? new UserEntry() { IsNew = true, GuildId = (long)user.Guild.Id, UserId = (long)user.Id };
=> db.UserEntries.Where(u => u.GuildId == user.Guild.Id && u.UserId == user.Id).FirstOrDefault()
?? new UserEntry() { IsNew = true, GuildId = user.Guild.Id, UserId = user.Id };
}

View file

@ -10,25 +10,27 @@ public class GuildConfig {
}
[Key]
[Column("guild_id")]
public long GuildId { get; set; }
public ulong GuildId { get; set; }
[Column("role_id")]
public long? BirthdayRole { get; set; }
public ulong? BirthdayRole { get; set; }
[Column("channel_announce_id")]
public long? AnnouncementChannel { get; set; }
public ulong? AnnouncementChannel { get; set; }
[Column("time_zone")]
public string? GuildTimeZone { get; set; }
[Column("moderated")]
public bool Moderated { get; set; }
[Column("moderator_role")]
public long? ModeratorRole { get; set; }
[Column("announce_message")]
public ulong? ModeratorRole { get; set; }
public string? AnnounceMessage { get; set; }
[Column("announce_message_pl")]
public string? AnnounceMessagePl { get; set; }
[Column("announce_ping")]
public bool AnnouncePing { get; set; }
[Column("last_seen")]
public DateTimeOffset LastSeen { get; set; }
[InverseProperty(nameof(BlocklistEntry.Guild))]

View file

@ -0,0 +1,161 @@
// <auto-generated />
using System;
using BirthdayBot.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BirthdayBot.Data.Migrations
{
[DbContext(typeof(BotDatabaseContext))]
[Migration("20221123062847_LongToUlong")]
partial class LongToUlong
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
{
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("user_id");
b.HasKey("GuildId", "UserId")
.HasName("banned_users_pkey");
b.ToTable("banned_users", (string)null);
});
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
{
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<string>("AnnounceMessage")
.HasColumnType("text")
.HasColumnName("announce_message");
b.Property<string>("AnnounceMessagePl")
.HasColumnType("text")
.HasColumnName("announce_message_pl");
b.Property<bool>("AnnouncePing")
.HasColumnType("boolean")
.HasColumnName("announce_ping");
b.Property<decimal?>("AnnouncementChannel")
.HasColumnType("numeric(20,0)")
.HasColumnName("channel_announce_id");
b.Property<decimal?>("BirthdayRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("role_id");
b.Property<string>("GuildTimeZone")
.HasColumnType("text")
.HasColumnName("time_zone");
b.Property<DateTimeOffset>("LastSeen")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen")
.HasDefaultValueSql("now()");
b.Property<bool>("Moderated")
.HasColumnType("boolean")
.HasColumnName("moderated");
b.Property<decimal?>("ModeratorRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("moderator_role");
b.HasKey("GuildId")
.HasName("settings_pkey");
b.ToTable("settings", (string)null);
});
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
{
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("user_id");
b.Property<int>("BirthDay")
.HasColumnType("integer")
.HasColumnName("birth_day");
b.Property<int>("BirthMonth")
.HasColumnType("integer")
.HasColumnName("birth_month");
b.Property<DateTimeOffset>("LastSeen")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasColumnName("last_seen")
.HasDefaultValueSql("now()");
b.Property<string>("TimeZone")
.HasColumnType("text")
.HasColumnName("time_zone");
b.HasKey("GuildId", "UserId")
.HasName("user_birthdays_pkey");
b.ToTable("user_birthdays", (string)null);
});
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
{
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
.WithMany("BlockedUsers")
.HasForeignKey("GuildId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("banned_users_guild_id_fkey");
b.Navigation("Guild");
});
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
{
b.HasOne("BirthdayBot.Data.GuildConfig", "Guild")
.WithMany("UserEntries")
.HasForeignKey("GuildId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("user_birthdays_guild_id_fkey");
b.Navigation("Guild");
});
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
{
b.Navigation("BlockedUsers");
b.Navigation("UserEntries");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,177 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BirthdayBot.Data.Migrations
{
/// <inheritdoc />
public partial class LongToUlong : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// NOTE: manually edited - must drop and re-add foreign key due to altered types
migrationBuilder.DropForeignKey(
name: "user_birthdays_guild_id_fkey",
table: "user_birthdays");
migrationBuilder.AlterColumn<decimal>(
name: "user_id",
table: "user_birthdays",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<decimal>(
name: "guild_id",
table: "user_birthdays",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<decimal>(
name: "role_id",
table: "settings",
type: "numeric(20,0)",
nullable: true,
oldClrType: typeof(long),
oldType: "bigint",
oldNullable: true);
migrationBuilder.AlterColumn<decimal>(
name: "moderator_role",
table: "settings",
type: "numeric(20,0)",
nullable: true,
oldClrType: typeof(long),
oldType: "bigint",
oldNullable: true);
migrationBuilder.AlterColumn<decimal>(
name: "channel_announce_id",
table: "settings",
type: "numeric(20,0)",
nullable: true,
oldClrType: typeof(long),
oldType: "bigint",
oldNullable: true);
migrationBuilder.AlterColumn<decimal>(
name: "guild_id",
table: "settings",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<decimal>(
name: "user_id",
table: "banned_users",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AlterColumn<decimal>(
name: "guild_id",
table: "banned_users",
type: "numeric(20,0)",
nullable: false,
oldClrType: typeof(long),
oldType: "bigint");
migrationBuilder.AddForeignKey(
name: "user_birthdays_guild_id_fkey",
table: "user_birthdays",
column: "guild_id",
principalTable: "settings",
principalColumn: "guild_id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "user_birthdays_guild_id_fkey",
table: "user_birthdays");
migrationBuilder.AlterColumn<long>(
name: "user_id",
table: "user_birthdays",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)");
migrationBuilder.AlterColumn<long>(
name: "guild_id",
table: "user_birthdays",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)");
migrationBuilder.AlterColumn<long>(
name: "role_id",
table: "settings",
type: "bigint",
nullable: true,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)",
oldNullable: true);
migrationBuilder.AlterColumn<long>(
name: "moderator_role",
table: "settings",
type: "bigint",
nullable: true,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)",
oldNullable: true);
migrationBuilder.AlterColumn<long>(
name: "channel_announce_id",
table: "settings",
type: "bigint",
nullable: true,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)",
oldNullable: true);
migrationBuilder.AlterColumn<long>(
name: "guild_id",
table: "settings",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)");
migrationBuilder.AlterColumn<long>(
name: "user_id",
table: "banned_users",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)");
migrationBuilder.AlterColumn<long>(
name: "guild_id",
table: "banned_users",
type: "bigint",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(20,0)");
migrationBuilder.AddForeignKey(
name: "user_birthdays_guild_id_fkey",
table: "user_birthdays",
column: "guild_id",
principalTable: "settings",
principalColumn: "guild_id",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -17,19 +17,19 @@ namespace BirthdayBot.Data.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("ProductVersion", "7.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("BirthdayBot.Data.BlocklistEntry", b =>
{
b.Property<long>("GuildId")
.HasColumnType("bigint")
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<long>("UserId")
.HasColumnType("bigint")
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("user_id");
b.HasKey("GuildId", "UserId")
@ -40,8 +40,8 @@ namespace BirthdayBot.Data.Migrations
modelBuilder.Entity("BirthdayBot.Data.GuildConfig", b =>
{
b.Property<long>("GuildId")
.HasColumnType("bigint")
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<string>("AnnounceMessage")
@ -56,10 +56,18 @@ namespace BirthdayBot.Data.Migrations
.HasColumnType("boolean")
.HasColumnName("announce_ping");
b.Property<long?>("ChannelAnnounceId")
.HasColumnType("bigint")
b.Property<decimal?>("AnnouncementChannel")
.HasColumnType("numeric(20,0)")
.HasColumnName("channel_announce_id");
b.Property<decimal?>("BirthdayRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("role_id");
b.Property<string>("GuildTimeZone")
.HasColumnType("text")
.HasColumnName("time_zone");
b.Property<DateTimeOffset>("LastSeen")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
@ -70,18 +78,10 @@ namespace BirthdayBot.Data.Migrations
.HasColumnType("boolean")
.HasColumnName("moderated");
b.Property<long?>("ModeratorRole")
.HasColumnType("bigint")
b.Property<decimal?>("ModeratorRole")
.HasColumnType("numeric(20,0)")
.HasColumnName("moderator_role");
b.Property<long?>("RoleId")
.HasColumnType("bigint")
.HasColumnName("role_id");
b.Property<string>("TimeZone")
.HasColumnType("text")
.HasColumnName("time_zone");
b.HasKey("GuildId")
.HasName("settings_pkey");
@ -90,12 +90,12 @@ namespace BirthdayBot.Data.Migrations
modelBuilder.Entity("BirthdayBot.Data.UserEntry", b =>
{
b.Property<long>("GuildId")
.HasColumnType("bigint")
b.Property<decimal>("GuildId")
.HasColumnType("numeric(20,0)")
.HasColumnName("guild_id");
b.Property<long>("UserId")
.HasColumnType("bigint")
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("user_id");
b.Property<int>("BirthDay")

View file

@ -6,18 +6,16 @@ namespace BirthdayBot.Data;
[Table("user_birthdays")]
public class UserEntry {
[Key]
[Column("guild_id")]
public long GuildId { get; set; }
public ulong GuildId { get; set; }
[Key]
[Column("user_id")]
public long UserId { get; set; }
[Column("birth_month")]
public ulong UserId { get; set; }
public int BirthMonth { get; set; }
[Column("birth_day")]
public int BirthDay { get; set; }
[Column("time_zone")]
public string? TimeZone { get; set; }
[Column("last_seen")]
public DateTimeOffset LastSeen { get; set; }
[ForeignKey(nameof(GuildConfig.GuildId))]

View file

@ -1,6 +1,5 @@
global using Discord;
global using Discord.WebSocket;
using BirthdayBot.BackgroundServices;
using Discord.Interactions;
using Microsoft.Extensions.DependencyInjection;
using System.Text;
@ -23,7 +22,7 @@ class ShardManager : IDisposable {
/// <summary>
/// Number of concurrent shard startups to happen on each check.
/// This value is also used in <see cref="DataRetention"/>.
/// This value also determines the maximum amount of concurrent background database operations.
/// </summary>
public const int MaxConcurrentOperations = 4;