mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 02:08:27 -04:00
WIP db provider support for Mysql and Postgres
This commit is contained in:
@@ -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();
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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; }
|
||||
|
||||
|
42
src/NadekoBot/Db/MysqlContext.cs
Normal file
42
src/NadekoBot/Db/MysqlContext.cs
Normal 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");
|
||||
}
|
||||
}
|
@@ -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();
|
||||
|
28
src/NadekoBot/Db/PostgreSqlContext.cs
Normal file
28
src/NadekoBot/Db/PostgreSqlContext.cs
Normal 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);
|
||||
}
|
||||
}
|
30
src/NadekoBot/Db/SqliteContext.cs
Normal file
30
src/NadekoBot/Db/SqliteContext.cs
Normal 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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user