WIP db provider support for Mysql and Postgres

This commit is contained in:
Kwoth
2022-04-11 10:41:26 +00:00
parent 8e1ec2ed9e
commit e23233ee06
66 changed files with 21891 additions and 382 deletions

View File

@@ -12,7 +12,7 @@ public static class ClubExtensions
.ThenInclude(x => x.User)
.Include(x => x.Bans)
.ThenInclude(x => x.User)
.Include(x => x.Users)
.Include(x => x.Members)
.AsQueryable();
public static ClubInfo GetByOwner(this DbSet<ClubInfo> clubs, ulong userId)
@@ -20,24 +20,14 @@ public static class ClubExtensions
public static ClubInfo GetByOwnerOrAdmin(this DbSet<ClubInfo> clubs, ulong userId)
=> Include(clubs)
.FirstOrDefault(c => c.Owner.UserId == userId || c.Users.Any(u => u.UserId == userId && u.IsClubAdmin));
.FirstOrDefault(c => c.Owner.UserId == userId || c.Members.Any(u => u.UserId == userId && u.IsClubAdmin));
public static ClubInfo GetByMember(this DbSet<ClubInfo> clubs, ulong userId)
=> Include(clubs).FirstOrDefault(c => c.Users.Any(u => u.UserId == userId));
=> Include(clubs).FirstOrDefault(c => c.Members.Any(u => u.UserId == userId));
public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name, int discrim)
public static ClubInfo GetByName(this DbSet<ClubInfo> clubs, string name)
=> Include(clubs)
.FirstOrDefault(c => EF.Functions.Collate(c.Name, "NOCASE") == EF.Functions.Collate(name, "NOCASE")
&& c.Discrim == discrim);
public static int GetNextDiscrim(this DbSet<ClubInfo> clubs, string name)
=> Include(clubs)
.Where(x =>
EF.Functions.Collate(x.Name, "NOCASE") == EF.Functions.Collate(name, "NOCASE"))
.Select(x => x.Discrim)
.DefaultIfEmpty()
.Max()
+ 1;
.FirstOrDefault(c => c.Name == name);
public static List<ClubInfo> GetClubLeaderboardPage(this DbSet<ClubInfo> clubs, int page)
=> clubs.AsNoTracking().OrderByDescending(x => x.Xp).Skip(page * 9).Take(9).ToList();

View File

@@ -9,6 +9,11 @@ namespace NadekoBot.Db;
public static class DiscordUserExtensions
{
public static Task<DiscordUser> GetByUserIdAsync(
this IQueryable<DiscordUser> set,
ulong userId)
=> set.FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId);
public static void EnsureUserCreated(
this NadekoContext ctx,
ulong userId,
@@ -37,20 +42,49 @@ public static class DiscordUserExtensions
UserId = userId
});
public static Task EnsureUserCreatedAsync(
this NadekoContext ctx,
ulong userId)
=> ctx.DiscordUser
.ToLinqToDBTable()
.InsertOrUpdateAsync(
() => new()
{
UserId = userId,
Username = "Unknown",
Discriminator = "????",
AvatarId = string.Empty,
TotalXp = 0,
CurrencyAmount = 0
},
old => new()
{
},
() => new()
{
UserId = userId
});
//temp is only used in updatecurrencystate, so that i don't overwrite real usernames/discrims with Unknown
public static DiscordUser GetOrCreateUser(
this NadekoContext ctx,
ulong userId,
string username,
string discrim,
string avatarId)
string avatarId,
Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
{
ctx.EnsureUserCreated(userId, username, discrim, avatarId);
return ctx.DiscordUser.Include(x => x.Club).First(u => u.UserId == userId);
IQueryable<DiscordUser> queryable = ctx.DiscordUser;
if (includes is not null)
queryable = includes(queryable);
return queryable.First(u => u.UserId == userId);
}
public static DiscordUser GetOrCreateUser(this NadekoContext ctx, IUser original)
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId);
public static DiscordUser GetOrCreateUser(this NadekoContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>> includes = null)
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
public static int GetUserGlobalRank(this DbSet<DiscordUser> users, ulong id)
=> users.AsQueryable()

View File

@@ -73,7 +73,6 @@ public static class GuildConfigExtensions
{
GuildConfig config;
// todo linq2db
if (includes is null)
config = ctx.GuildConfigs.IncludeEverything().FirstOrDefault(c => c.GuildId == guildId);
else

View File

@@ -8,24 +8,19 @@ public class ClubInfo : DbEntity
{
[MaxLength(20)]
public string Name { get; set; }
public int Discrim { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; } = string.Empty;
public int MinimumLevelReq { get; set; } = 5;
public int Xp { get; set; } = 0;
public int OwnerId { get; set; }
public int? OwnerId { get; set; }
public DiscordUser Owner { get; set; }
public List<DiscordUser> Users { get; set; } = new();
public List<DiscordUser> Members { get; set; } = new();
public List<ClubApplicants> Applicants { get; set; } = new();
public List<ClubBans> Bans { get; set; } = new();
public string Description { get; set; }
public override string ToString()
=> Name + "#" + Discrim;
=> Name;
}
public class ClubApplicants

View File

@@ -10,6 +10,7 @@ public class DiscordUser : DbEntity
public string Discriminator { get; set; }
public string AvatarId { get; set; }
public int? ClubId { get; set; }
public ClubInfo Club { get; set; }
public bool IsClubAdmin { get; set; }

View File

@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using NadekoBot.Db.Models;
namespace NadekoBot.Services.Database;
public sealed class MysqlContext : NadekoContext
{
private readonly string _connStr;
private readonly string _version;
protected override string CurrencyTransactionOtherIdDefaultValue
=> "NULL";
protected override string DiscordUserLastXpGainDefaultValue
=> "(UTC_TIMESTAMP - INTERVAL 1 year)";
protected override string LastLevelUpDefaultValue
=> "(UTC_TIMESTAMP)";
public MysqlContext(string connStr = "Server=localhost", string version = "8.0")
{
_connStr = connStr;
_version = version;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
.UseLowerCaseNamingConvention()
.UseMySql(_connStr, ServerVersion.Parse(_version));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// mysql is case insensitive by default
// we can set binary collation to change that
modelBuilder.Entity<ClubInfo>()
.Property(x => x.Name)
.UseCollation("utf8mb4_bin");
}
}

View File

@@ -1,5 +1,4 @@
#nullable disable
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
@@ -10,23 +9,7 @@ using NadekoBot.Services.Database.Models;
namespace NadekoBot.Services.Database;
public class NadekoContextFactory : IDesignTimeDbContextFactory<NadekoContext>
{
public NadekoContext CreateDbContext(string[] args)
{
LogSetup.SetupLogger(-2);
var optionsBuilder = new DbContextOptionsBuilder<NadekoContext>();
var creds = new BotCredsProvider().GetCreds();
var builder = new SqliteConnectionStringBuilder(creds.Db.ConnectionString);
builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
optionsBuilder.UseSqlite(builder.ToString());
var ctx = new NadekoContext(optionsBuilder.Options);
ctx.Database.SetCommandTimeout(60);
return ctx;
}
}
public class NadekoContext : DbContext
public abstract class NadekoContext : DbContext
{
public DbSet<GuildConfig> GuildConfigs { get; set; }
@@ -69,11 +52,14 @@ public class NadekoContext : DbContext
public DbSet<Permissionv2> Permissions { get; set; }
public NadekoContext(DbContextOptions<NadekoContext> options)
: base(options)
{
}
#region Mandatory Provider-Specific Values
protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
protected abstract string DiscordUserLastXpGainDefaultValue { get; }
protected abstract string LastLevelUpDefaultValue { get; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
#region QUOTES
@@ -167,10 +153,10 @@ public class NadekoContext : DbContext
.HasDefaultValue(XpNotificationLocation.None);
du.Property(x => x.LastXpGain)
.HasDefaultValueSql("datetime('now', '-1 years')");
.HasDefaultValueSql(DiscordUserLastXpGainDefaultValue);
du.Property(x => x.LastLevelUp)
.HasDefaultValueSql("datetime('now')");
.HasDefaultValueSql(LastLevelUpDefaultValue);
du.Property(x => x.TotalXp)
.HasDefaultValue(0);
@@ -179,7 +165,10 @@ public class NadekoContext : DbContext
.HasDefaultValue(0);
du.HasAlternateKey(w => w.UserId);
du.HasOne(x => x.Club).WithMany(x => x.Users).IsRequired(false);
du.HasOne(x => x.Club)
.WithMany(x => x.Members)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
du.HasIndex(x => x.TotalXp);
du.HasIndex(x => x.CurrencyAmount);
@@ -218,7 +207,7 @@ public class NadekoContext : DbContext
.IsUnique();
xps.Property(x => x.LastLevelUp)
.HasDefaultValue(new DateTime(2017, 9, 21, 20, 53, 13, 307, DateTimeKind.Local));
.HasDefaultValueSql(LastLevelUpDefaultValue);
xps.HasIndex(x => x.UserId);
xps.HasIndex(x => x.GuildId);
@@ -248,13 +237,14 @@ public class NadekoContext : DbContext
#region Club
var ci = modelBuilder.Entity<ClubInfo>();
ci.HasOne(x => x.Owner).WithOne().HasForeignKey<ClubInfo>(x => x.OwnerId);
ci.HasOne(x => x.Owner)
.WithOne()
.HasForeignKey<ClubInfo>(x => x.OwnerId)
.OnDelete(DeleteBehavior.SetNull);
ci.HasAlternateKey(x => new
{
x.Name,
x.Discrim
x.Name
});
#endregion
@@ -268,9 +258,13 @@ public class NadekoContext : DbContext
t.UserId
});
modelBuilder.Entity<ClubApplicants>().HasOne(pt => pt.User).WithMany();
modelBuilder.Entity<ClubApplicants>()
.HasOne(pt => pt.User)
.WithMany();
modelBuilder.Entity<ClubApplicants>().HasOne(pt => pt.Club).WithMany(x => x.Applicants);
modelBuilder.Entity<ClubApplicants>()
.HasOne(pt => pt.Club)
.WithMany(x => x.Applicants);
modelBuilder.Entity<ClubBans>()
.HasKey(t => new
@@ -279,9 +273,13 @@ public class NadekoContext : DbContext
t.UserId
});
modelBuilder.Entity<ClubBans>().HasOne(pt => pt.User).WithMany();
modelBuilder.Entity<ClubBans>()
.HasOne(pt => pt.User)
.WithMany();
modelBuilder.Entity<ClubBans>().HasOne(pt => pt.Club).WithMany(x => x.Bans);
modelBuilder.Entity<ClubBans>()
.HasOne(pt => pt.Club)
.WithMany(x => x.Bans);
#endregion
@@ -299,7 +297,7 @@ public class NadekoContext : DbContext
.IsUnique(false);
e.Property(x => x.OtherId)
.HasDefaultValueSql("NULL");
.HasDefaultValueSql(CurrencyTransactionOtherIdDefaultValue);
e.Property(x => x.Type)
.IsRequired();

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore;
namespace NadekoBot.Services.Database;
public sealed class PostgreSqlContext : NadekoContext
{
private readonly string _connStr;
protected override string CurrencyTransactionOtherIdDefaultValue
=> "NULL";
protected override string DiscordUserLastXpGainDefaultValue
=> "timezone('utc', now()) - interval '-1 year'";
protected override string LastLevelUpDefaultValue
=> "timezone('utc', now())";
public PostgreSqlContext(string connStr = "Host=localhost")
{
_connStr = connStr;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
.UseLowerCaseNamingConvention()
.UseNpgsql(_connStr);
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace NadekoBot.Services.Database;
public sealed class SqliteContext : NadekoContext
{
private readonly string _connectionString;
protected override string CurrencyTransactionOtherIdDefaultValue
=> "NULL";
protected override string DiscordUserLastXpGainDefaultValue
=> "datetime('now', '-1 years')";
protected override string LastLevelUpDefaultValue
=> "datetime('now')";
public SqliteContext(string connectionString = "Data Source=data/NadekoBot.db", int commandTimeout = 60)
{
_connectionString = connectionString;
Database.SetCommandTimeout(commandTimeout);
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
var builder = new SqliteConnectionStringBuilder(_connectionString);
builder.DataSource = Path.Combine(AppContext.BaseDirectory, builder.DataSource);
optionsBuilder.UseSqlite(builder.ToString());
}
}