Rewrite GuildUserConfiguration

Corresponding to GuildConfiguration, no longer assumes it is cached.
This commit is contained in:
Noi 2020-07-16 13:21:33 -07:00
parent 7ac15e21a1
commit 488685bc79

View file

@ -8,161 +8,143 @@ using System.Threading.Tasks;
namespace BirthdayBot.Data namespace BirthdayBot.Data
{ {
/// <summary> /// <summary>
/// Representation of a user's birthday settings within a guild. /// Represents configuration for a guild user.
/// Instances are held and managed by <see cref="="GuildStateInformation"/>.
/// </summary> /// </summary>
class GuildUserSettings class GuildUserConfiguration
{ {
private int _month;
private int _day;
private string _tz;
public ulong GuildId { get; } public ulong GuildId { get; }
public ulong UserId { get; } public ulong UserId { get; }
/// <summary> /// <summary>
/// Month of birth as a numeric value. Range 1-12. /// Month of birth as a numeric value. Range 1-12.
/// </summary> /// </summary>
public int BirthMonth { get { return _month; } } public int BirthMonth { get; private set; }
/// <summary> /// <summary>
/// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value. /// Day of birth as a numeric value. Ranges between 1-31 or lower based on month value.
/// </summary> /// </summary>
public int BirthDay { get { return _day; } } public int BirthDay { get; private set; }
public string TimeZone { get { return _tz; } } public string TimeZone { get; private set; }
public bool IsKnown { get { return _month != 0 && _day != 0; } } public bool IsKnown { get { return BirthMonth != 0 && BirthDay != 0; } }
/// <summary> /// <summary>
/// Creates a data-less instance without any useful information. /// Creates a new, data-less instance without a corresponding database entry.
/// Calling <see cref="UpdateAsync(int, int, int)"/> will create a real database enty /// Calling <see cref="UpdateAsync(int, int, int)"/> will create a real database enty
/// </summary> /// </summary>
public GuildUserSettings(ulong guildId, ulong userId) private GuildUserConfiguration(ulong guildId, ulong userId)
{ {
GuildId = guildId; GuildId = guildId;
UserId = userId; UserId = userId;
} }
// Called by GetGuildUsersAsync. Double-check ordinals when changes are made. // Called by GetGuildUsersAsync. Double-check ordinals when changes are made.
private GuildUserSettings(DbDataReader reader) private GuildUserConfiguration(DbDataReader reader)
{ {
GuildId = (ulong)reader.GetInt64(0); GuildId = (ulong)reader.GetInt64(0);
UserId = (ulong)reader.GetInt64(1); UserId = (ulong)reader.GetInt64(1);
_month = reader.GetInt32(2); BirthMonth = reader.GetInt32(2);
_day = reader.GetInt32(3); BirthDay = reader.GetInt32(3);
if (!reader.IsDBNull(4)) _tz = reader.GetString(4); if (!reader.IsDBNull(4)) TimeZone = reader.GetString(4);
} }
/// <summary> /// <summary>
/// Updates user with given information. /// Updates user with given information.
/// NOTE: If there exists a tz value and the update contains none, the old tz value is retained.
/// </summary> /// </summary>
public async Task UpdateAsync(int month, int day, string newtz, Database dbconfig) public async Task UpdateAsync(int month, int day, string newtz)
{ {
// TODO note from rewrite: huh? why are we doing this here? using (var db = await Database.OpenConnectionAsync())
var inserttz = newtz ?? TimeZone;
using (var db = await dbconfig.OpenConnectionAsync())
{ {
// Will do a delete/insert instead of insert...on conflict update. Because lazy. using var c = db.CreateCommand();
using (var t = db.BeginTransaction()) c.CommandText = $"insert into {BackingTable} "
{ + "(guild_id, user_id, birth_month, birth_day, time_zone) values "
await DoDeleteAsync(db); + "(@Gid, @Uid, @Month, @Day, @Tz) "
using (var c = db.CreateCommand()) + "on conflict (guild_id, user_id) do update "
{ + "set birth_month = EXCLUDED.birth_month, birth_day = EXCLUDED.birth_day, time_zone = EXCLUDED.time_zone";
c.CommandText = $"insert into {BackingTable} " c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
+ "(guild_id, user_id, birth_month, birth_day, time_zone) values " c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
+ "(@Gid, @Uid, @Month, @Day, @Tz)"; c.Parameters.Add("@Month", NpgsqlDbType.Numeric).Value = month;
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId; c.Parameters.Add("@Day", NpgsqlDbType.Numeric).Value = day;
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId; var tzp = c.Parameters.Add("@Tz", NpgsqlDbType.Text);
c.Parameters.Add("@Month", NpgsqlDbType.Numeric).Value = month; if (newtz != null) tzp.Value = newtz;
c.Parameters.Add("@Day", NpgsqlDbType.Numeric).Value = day; else tzp.Value = DBNull.Value;
var p = c.Parameters.Add("@Tz", NpgsqlDbType.Text); c.Prepare();
if (inserttz != null) p.Value = inserttz; await c.ExecuteNonQueryAsync();
else p.Value = DBNull.Value;
c.Prepare();
await c.ExecuteNonQueryAsync();
}
await t.CommitAsync();
}
} }
// We didn't crash! Get the new values stored locally. // Database update succeeded; update instance values
_month = month; BirthMonth = month;
_day = day; BirthDay = day;
_tz = inserttz; TimeZone = newtz;
} }
/// <summary> /// <summary>
/// Deletes information of this user from the backing database. /// Deletes information of this user from the backing database.
/// The corresponding object reference should ideally be discarded after calling this. /// The corresponding object reference should ideally be discarded after calling this.
/// </summary> /// </summary>
public async Task DeleteAsync(Database dbconfig) public async Task DeleteAsync()
{ {
using (var db = await dbconfig.OpenConnectionAsync()) using var db = await Database.OpenConnectionAsync();
{ using var c = db.CreateCommand();
await DoDeleteAsync(db); c.CommandText = $"delete from {BackingTable} "
} + "where guild_id = @Gid and user_id = @Uid";
} c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
// Shared between UpdateAsync and DeleteAsync c.Prepare();
private async Task DoDeleteAsync(NpgsqlConnection dbconn) await c.ExecuteNonQueryAsync();
{
using (var c = dbconn.CreateCommand())
{
c.CommandText = $"delete from {BackingTable} "
+ "where guild_id = @Gid and user_id = @Uid";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)GuildId;
c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)UserId;
c.Prepare();
await c.ExecuteNonQueryAsync();
}
} }
#region Database #region Database
public const string BackingTable = "user_birthdays"; public const string BackingTable = "user_birthdays";
// Take note of ordinals for use in the constructor
private const string SelectFields = "guild_id, user_id, birth_month, birth_day, time_zone";
internal static void SetUpDatabaseTable(NpgsqlConnection db) internal static async Task DatabaseSetupAsync(NpgsqlConnection db)
{ {
using (var c = db.CreateCommand()) using var c = db.CreateCommand();
{ c.CommandText = $"create table if not exists {BackingTable} ("
c.CommandText = $"create table if not exists {BackingTable} (" + $"guild_id bigint not null references {GuildConfiguration.BackingTable} ON DELETE CASCADE, "
+ $"guild_id bigint not null references {GuildStateInformation.BackingTable} ON DELETE CASCADE, " + "user_id bigint not null, "
+ "user_id bigint not null, " + "birth_month integer not null, "
+ "birth_month integer not null, " + "birth_day integer not null, "
+ "birth_day integer not null, " + "time_zone text null, "
+ "time_zone text null, " + "last_seen timestamptz not null default NOW(), "
+ "last_seen timestamptz not null default NOW(), " + "PRIMARY KEY (guild_id, user_id)" // index automatically created with this
+ "PRIMARY KEY (guild_id, user_id)" + ")";
+ ")"; await c.ExecuteNonQueryAsync();
c.ExecuteNonQuery();
}
} }
/// <summary> /// <summary>
/// Gets all known birthday records from the specified guild. No further filtering is done here. /// Attempts to retrieve a user's configuration. Returns a new, updateable instance if none is found.
/// </summary> /// </summary>
internal static IEnumerable<GuildUserSettings> GetGuildUsersAsync(Database dbsettings, ulong guildId) public static async Task<GuildUserConfiguration> LoadAsync(ulong guildId, ulong userId)
{ {
using (var db = dbsettings.OpenConnectionAsync().GetAwaiter().GetResult()) using var db = await Database.OpenConnectionAsync();
{ using var c = db.CreateCommand();
using (var c = db.CreateCommand()) c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid and user_id = @Uid";
{ c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
// Take note of ordinals for use in the constructor c.Parameters.Add("@Uid", NpgsqlDbType.Bigint).Value = (long)userId;
c.CommandText = "select guild_id, user_id, birth_month, birth_day, time_zone " c.Prepare();
+ $"from {BackingTable} where guild_id = @Gid";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId; using var r = c.ExecuteReader();
c.Prepare(); if (await r.ReadAsync()) return new GuildUserConfiguration(r);
using (var r = c.ExecuteReader()) else return new GuildUserConfiguration(guildId, userId);
{ }
var result = new List<GuildUserSettings>();
while (r.Read()) /// <summary>
{ /// Gets all known user configuration records associated with the specified guild.
result.Add(new GuildUserSettings(r)); /// </summary>
} public static async Task<IEnumerable<GuildUserConfiguration>> LoadAllAsync(ulong guildId)
return result; {
} using var db = await Database.OpenConnectionAsync();
} using var c = db.CreateCommand();
} c.CommandText = $"select {SelectFields} from {BackingTable} where guild_id = @Gid";
c.Parameters.Add("@Gid", NpgsqlDbType.Bigint).Value = (long)guildId;
c.Prepare();
using var r = await c.ExecuteReaderAsync();
var result = new List<GuildUserConfiguration>();
while (await r.ReadAsync()) result.Add(new GuildUserConfiguration(r));
return result;
} }
#endregion #endregion
} }