Added postgres support (#556)

This commit is contained in:
DamianX
2020-01-24 17:25:01 +01:00
committed by Pieter-Jan Briers
parent f95c5b7921
commit 514d05b237
19 changed files with 566 additions and 43 deletions

View File

@@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore;
using Npgsql;
namespace Content.Server.Database
{
public interface IDatabaseConfiguration
{
DbContextOptions<PreferencesDbContext> Options { get; }
}
public class PostgresConfiguration : IDatabaseConfiguration
{
private readonly string _database;
private readonly string _host;
private readonly string _password;
private readonly int _port;
private readonly string _username;
public PostgresConfiguration(string host,
int port,
string database,
string username,
string password)
{
_host = host;
_port = port;
_database = database;
_username = username;
_password = password;
}
public DbContextOptions<PreferencesDbContext> Options
{
get
{
var optionsBuilder = new DbContextOptionsBuilder<PreferencesDbContext>();
var connectionString = new NpgsqlConnectionStringBuilder
{
Host = _host,
Port = _port,
Database = _database,
Username = _username,
Password = _password
}.ConnectionString;
optionsBuilder.UseNpgsql(connectionString);
return optionsBuilder.Options;
}
}
}
public class SqliteConfiguration : IDatabaseConfiguration
{
private readonly string _databaseFilePath;
public SqliteConfiguration(string databaseFilePath)
{
_databaseFilePath = databaseFilePath;
}
public DbContextOptions<PreferencesDbContext> Options
{
get
{
var optionsBuilder = new DbContextOptionsBuilder<PreferencesDbContext>();
optionsBuilder.UseSqlite($"Data Source={_databaseFilePath}");
return optionsBuilder.Options;
}
}
}
}

View File

@@ -19,6 +19,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="3.1.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,151 @@
// <auto-generated />
using Content.Server.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
[DbContext(typeof(PostgresPreferencesDbContext))]
[Migration("20200124133512_InitialPg")]
partial class InitialPg
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{
b.Property<int>("HumanoidProfileId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("Age")
.HasColumnType("integer");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("HairName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("PreferenceUnavailable")
.HasColumnType("integer");
b.Property<int>("PrefsId")
.HasColumnType("integer");
b.Property<string>("Sex")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Slot")
.HasColumnType("integer");
b.Property<string>("SlotName")
.IsRequired()
.HasColumnType("text");
b.HasKey("HumanoidProfileId");
b.HasIndex("PrefsId");
b.ToTable("HumanoidProfile");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("JobId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<int>("ProfileHumanoidProfileId")
.HasColumnType("integer");
b.HasKey("JobId");
b.HasIndex("ProfileHumanoidProfileId");
b.ToTable("Job");
});
modelBuilder.Entity("Content.Server.Database.Prefs", b =>
{
b.Property<int>("PrefsId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("SelectedCharacterSlot")
.HasColumnType("integer");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("PrefsId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Preferences");
});
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{
b.HasOne("Content.Server.Database.Prefs", "Prefs")
.WithMany("HumanoidProfiles")
.HasForeignKey("PrefsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.HumanoidProfile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileHumanoidProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,105 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
public partial class InitialPg : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Preferences",
columns: table => new
{
PrefsId = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(nullable: false),
SelectedCharacterSlot = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Preferences", x => x.PrefsId);
});
migrationBuilder.CreateTable(
name: "HumanoidProfile",
columns: table => new
{
HumanoidProfileId = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Slot = table.Column<int>(nullable: false),
SlotName = table.Column<string>(nullable: false),
CharacterName = table.Column<string>(nullable: false),
Age = table.Column<int>(nullable: false),
Sex = table.Column<string>(nullable: false),
HairName = table.Column<string>(nullable: false),
HairColor = table.Column<string>(nullable: false),
FacialHairName = table.Column<string>(nullable: false),
FacialHairColor = table.Column<string>(nullable: false),
EyeColor = table.Column<string>(nullable: false),
SkinColor = table.Column<string>(nullable: false),
PreferenceUnavailable = table.Column<int>(nullable: false),
PrefsId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HumanoidProfile", x => x.HumanoidProfileId);
table.ForeignKey(
name: "FK_HumanoidProfile_Preferences_PrefsId",
column: x => x.PrefsId,
principalTable: "Preferences",
principalColumn: "PrefsId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Job",
columns: table => new
{
JobId = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProfileHumanoidProfileId = table.Column<int>(nullable: false),
JobName = table.Column<string>(nullable: false),
Priority = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Job", x => x.JobId);
table.ForeignKey(
name: "FK_Job_HumanoidProfile_ProfileHumanoidProfileId",
column: x => x.ProfileHumanoidProfileId,
principalTable: "HumanoidProfile",
principalColumn: "HumanoidProfileId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_HumanoidProfile_PrefsId",
table: "HumanoidProfile",
column: "PrefsId");
migrationBuilder.CreateIndex(
name: "IX_Job_ProfileHumanoidProfileId",
table: "Job",
column: "ProfileHumanoidProfileId");
migrationBuilder.CreateIndex(
name: "IX_Preferences_Username",
table: "Preferences",
column: "Username",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Job");
migrationBuilder.DropTable(
name: "HumanoidProfile");
migrationBuilder.DropTable(
name: "Preferences");
}
}
}

View File

@@ -0,0 +1,148 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace Content.Server.Database.Migrations.Postgres
{
[DbContext(typeof(PostgresPreferencesDbContext))]
partial class PostgresPreferencesDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
.HasAnnotation("ProductVersion", "3.1.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{
b.Property<int>("HumanoidProfileId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("Age")
.HasColumnType("integer");
b.Property<string>("CharacterName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("EyeColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FacialHairColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("FacialHairName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("HairColor")
.IsRequired()
.HasColumnType("text");
b.Property<string>("HairName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("PreferenceUnavailable")
.HasColumnType("integer");
b.Property<int>("PrefsId")
.HasColumnType("integer");
b.Property<string>("Sex")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SkinColor")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Slot")
.HasColumnType("integer");
b.Property<string>("SlotName")
.IsRequired()
.HasColumnType("text");
b.HasKey("HumanoidProfileId");
b.HasIndex("PrefsId");
b.ToTable("HumanoidProfile");
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.Property<int>("JobId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<string>("JobName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<int>("ProfileHumanoidProfileId")
.HasColumnType("integer");
b.HasKey("JobId");
b.HasIndex("ProfileHumanoidProfileId");
b.ToTable("Job");
});
modelBuilder.Entity("Content.Server.Database.Prefs", b =>
{
b.Property<int>("PrefsId")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
b.Property<int>("SelectedCharacterSlot")
.HasColumnType("integer");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("PrefsId");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Preferences");
});
modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b =>
{
b.HasOne("Content.Server.Database.Prefs", "Prefs")
.WithMany("HumanoidProfiles")
.HasForeignKey("PrefsId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Content.Server.Database.Job", b =>
{
b.HasOne("Content.Server.Database.HumanoidProfile", "Profile")
.WithMany("Jobs")
.HasForeignKey("ProfileHumanoidProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,8 +1,7 @@
// <auto-generated /> // <auto-generated />
using Content.Server.Database;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Content.Server.Database.Migrations namespace Content.Server.Database.Migrations
{ {

View File

@@ -3,16 +3,53 @@ using Microsoft.EntityFrameworkCore;
namespace Content.Server.Database namespace Content.Server.Database
{ {
public class PreferencesDbContext : DbContext public class PostgresPreferencesDbContext : PreferencesDbContext
{ {
// This is used by the "dotnet ef" CLI tool. // This is used by the "dotnet ef" CLI tool.
public PreferencesDbContext() : public PostgresPreferencesDbContext()
base(new DbContextOptionsBuilder().UseSqlite("Data Source=:memory:").Options)
{ {
} }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
if(!InitializedWithOptions)
options.UseNpgsql("dummy connection string");
}
public PostgresPreferencesDbContext(DbContextOptions<PreferencesDbContext> options) : base(options)
{
}
}
public class SqlitePreferencesDbContext : PreferencesDbContext
{
public SqlitePreferencesDbContext()
{
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
if (!InitializedWithOptions)
options.UseSqlite("dummy connection string");
}
public SqlitePreferencesDbContext(DbContextOptions<PreferencesDbContext> options) : base(options)
{
}
}
public abstract class PreferencesDbContext : DbContext
{
/// <summary>
/// The "dotnet ef" CLI tool uses the parameter-less constructor.
/// When that happens we want to supply the <see cref="DbContextOptions"/> via <see cref="DbContext.OnConfiguring"/>.
/// To use the context within the application, the options need to be passed the constructor instead.
/// </summary>
protected readonly bool InitializedWithOptions;
public PreferencesDbContext()
{
}
public PreferencesDbContext(DbContextOptions<PreferencesDbContext> options) : base(options) public PreferencesDbContext(DbContextOptions<PreferencesDbContext> options) : base(options)
{ {
InitializedWithOptions = true;
} }
public DbSet<Prefs> Preferences { get; set; } = null!; public DbSet<Prefs> Preferences { get; set; } = null!;

View File

@@ -1,3 +1,4 @@
using System;
using System.Linq; using System.Linq;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -7,12 +8,15 @@ namespace Content.Server.Database
{ {
private readonly PreferencesDbContext _prefsCtx; private readonly PreferencesDbContext _prefsCtx;
public PrefsDb(string dbPath) public PrefsDb(IDatabaseConfiguration dbConfig)
{ {
var optionsBuilder = new DbContextOptionsBuilder<PreferencesDbContext>(); _prefsCtx = dbConfig switch
optionsBuilder.UseSqlite($"Data Source={dbPath}"); {
SqliteConfiguration sqlite => (PreferencesDbContext) new SqlitePreferencesDbContext(
_prefsCtx = new PreferencesDbContext(optionsBuilder.Options); sqlite.Options),
PostgresConfiguration postgres => new PostgresPreferencesDbContext(postgres.Options),
_ => throw new NotImplementedException()
};
_prefsCtx.Database.Migrate(); _prefsCtx.Database.Migrate();
} }

View File

@@ -8,6 +8,5 @@ namespace Content.Server.Interfaces
void Initialize(); void Initialize();
void OnClientConnected(IPlayerSession session); void OnClientConnected(IPlayerSession session);
PlayerPreferences GetPreferences(string username); PlayerPreferences GetPreferences(string username);
void SavePreferences(PlayerPreferences prefs, string username);
} }
} }

View File

@@ -16,10 +16,10 @@ namespace Content.Server.Preferences
private readonly int _maxCharacterSlots; private readonly int _maxCharacterSlots;
private readonly PrefsDb _prefsDb; private readonly PrefsDb _prefsDb;
public PreferencesDatabase(string databaseFilePath, int maxCharacterSlots) public PreferencesDatabase(IDatabaseConfiguration dbConfig, int maxCharacterSlots)
{ {
_maxCharacterSlots = maxCharacterSlots; _maxCharacterSlots = maxCharacterSlots;
_prefsDb = new PrefsDb(databaseFilePath); _prefsDb = new PrefsDb(dbConfig);
} }
public PlayerPreferences GetPlayerPreferences(string username) public PlayerPreferences GetPlayerPreferences(string username)

View File

@@ -1,4 +1,6 @@
using System;
using System.IO; using System.IO;
using Content.Server.Database;
using Content.Server.Interfaces; using Content.Server.Interfaces;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Server.Interfaces.Player; using Robust.Server.Interfaces.Player;
@@ -31,14 +33,42 @@ namespace Content.Server.Preferences
HandleUpdateCharacterMessage); HandleUpdateCharacterMessage);
_configuration.RegisterCVar("game.maxcharacterslots", 10); _configuration.RegisterCVar("game.maxcharacterslots", 10);
_configuration.RegisterCVar("game.preferencesdbpath", "preferences.db"); _configuration.RegisterCVar("database.prefs_engine", "sqlite");
_configuration.RegisterCVar("database.prefs_sqlite_dbpath", "preferences.db");
_configuration.RegisterCVar("database.prefs_pg_host", "localhost");
_configuration.RegisterCVar("database.prefs_pg_port", 5432);
_configuration.RegisterCVar("database.prefs_pg_database", "ss14_prefs");
_configuration.RegisterCVar("database.prefs_pg_username", string.Empty);
_configuration.RegisterCVar("database.prefs_pg_password", string.Empty);
var configPreferencesDbPath = _configuration.GetCVar<string>("game.preferencesdbpath"); var engine = _configuration.GetCVar<string>("database.prefs_engine").ToLower();
var finalPreferencesDbPath = Path.Combine(_resourceManager.UserData.RootDir, configPreferencesDbPath); IDatabaseConfiguration dbConfig;
switch (engine)
{
case "sqlite":
var configPreferencesDbPath = _configuration.GetCVar<string>("database.prefs_sqlite_dbpath");
var finalPreferencesDbPath =
Path.Combine(_resourceManager.UserData.RootDir, configPreferencesDbPath);
dbConfig = new SqliteConfiguration(
finalPreferencesDbPath
);
break;
case "postgres":
dbConfig = new PostgresConfiguration(
_configuration.GetCVar<string>("database.prefs_pg_host"),
_configuration.GetCVar<int>("database.prefs_pg_port"),
_configuration.GetCVar<string>("database.prefs_pg_database"),
_configuration.GetCVar<string>("database.prefs_pg_username"),
_configuration.GetCVar<string>("database.prefs_pg_password")
);
break;
default:
throw new NotImplementedException("Unknown database engine {engine}.");
}
var maxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots"); var maxCharacterSlots = _configuration.GetCVar<int>("game.maxcharacterslots");
_preferencesDb = new PreferencesDatabase(finalPreferencesDbPath, maxCharacterSlots); _preferencesDb = new PreferencesDatabase(dbConfig, maxCharacterSlots);
} }
private void HandleSelectCharacterMessage(MsgSelectCharacter message) private void HandleSelectCharacterMessage(MsgSelectCharacter message)
@@ -79,25 +109,12 @@ namespace Content.Server.Preferences
var prefs = GetFromSql(username); var prefs = GetFromSql(username);
if (prefs is null) if (prefs is null)
{ {
prefs = PlayerPreferences.Default(); // TODO: Create random character instead _preferencesDb.SaveSelectedCharacterIndex(username, 0);
SavePreferences(prefs, username); _preferencesDb.SaveCharacterSlot(username, HumanoidCharacterProfile.Default(), 0);
prefs = GetFromSql(username);
} }
return prefs; return prefs;
} }
/// <summary>
/// Saves the given preferences to storage.
/// </summary>
public void SavePreferences(PlayerPreferences prefs, string username)
{
_preferencesDb.SaveSelectedCharacterIndex(username, prefs.SelectedCharacterIndex);
var index = 0;
foreach (var character in prefs.Characters)
{
_preferencesDb.SaveCharacterSlot(username, character, index);
index++;
}
}
} }
} }

View File

@@ -38,15 +38,6 @@ namespace Content.Shared.Preferences
public int FirstEmptySlot => IndexOfCharacter(null); public int FirstEmptySlot => IndexOfCharacter(null);
public static PlayerPreferences Default()
{
return new PlayerPreferences(new List<ICharacterProfile>
{
HumanoidCharacterProfile.Default()
},
0);
}
public int IndexOfCharacter(ICharacterProfile profile) public int IndexOfCharacter(ICharacterProfile profile)
{ {
return _characters.FindIndex(x => x == profile); return _characters.FindIndex(x => x == profile);

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Content.Server.Database;
using Content.Server.Preferences; using Content.Server.Preferences;
using Content.Shared; using Content.Shared;
using Content.Shared.Preferences; using Content.Shared.Preferences;
@@ -39,7 +40,7 @@ namespace Content.Tests.Server.Preferences
private static PreferencesDatabase GetDb() private static PreferencesDatabase GetDb()
{ {
return new PreferencesDatabase(Path.GetTempFileName(), MaxCharacterSlots); return new PreferencesDatabase(new SqliteConfiguration(Path.GetTempFileName()), MaxCharacterSlots);
} }
[Test] [Test]