diff --git a/Startup.cs b/Startup.cs index 10c8a88..29f342f 100644 --- a/Startup.cs +++ b/Startup.cs @@ -84,6 +84,11 @@ s.AddSingleton(); s.AddSingleton(); // Request handlers +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); +s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); @@ -96,10 +101,6 @@ s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); -s.AddSingleton(); -s.AddSingleton(); -s.AddSingleton(); -s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); s.AddSingleton(); diff --git a/Store/ChatterStore.cs b/Store/ChatterStore.cs index a79eec4..b65169f 100644 --- a/Store/ChatterStore.cs +++ b/Store/ChatterStore.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HermesSocketLibrary.db; using HermesSocketServer.Models; @@ -23,7 +24,7 @@ namespace HermesSocketServer.Store { "ttsVoiceId", "VoiceId" }, { "userId", "UserId" }, }; - _generator = new GroupSaveSqlGenerator(ctp); + _generator = new GroupSaveSqlGenerator(ctp, _logger); } public override async Task Load() @@ -55,46 +56,53 @@ namespace HermesSocketServer.Store { } - public override async Task Save() + public override async Task Save() { int count = 0; string sql = string.Empty; + ImmutableList? list = null; if (_added.Any()) { lock (_lock) { - count = _added.Count; - sql = _generator.GenerateInsertSql("TtsChatVoice", _added.Select(a => _store[a]), ["userId", "chatterId", "ttsVoiceId"]); + list = _added.ToImmutableList(); _added.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedInsertSql("TtsChatVoice", count, ["userId", "chatterId", "ttsVoiceId"]); - _logger.Debug($"TtsChatVoice - Adding {count} rows to database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + _logger.Debug($"User - Adding {count} rows to database: {sql}"); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_modified.Any()) { lock (_lock) { - count = _modified.Count; - sql = _generator.GenerateUpdateSql("TtsChatVoice", _modified.Select(m => _store[m]), ["userId", "chatterId"], ["ttsVoiceId"]); + list = _modified.ToImmutableList(); _modified.Clear(); } - _logger.Debug($"TtsChatVoice - Modifying {count} rows in database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + count = list.Count; + sql = _generator.GeneratePreparedUpdateSql("TtsChatVoice", count, ["userId", "chatterId"], ["ttsVoiceId"]); + + _logger.Debug($"User - Modifying {count} rows in database: {sql}"); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_deleted.Any()) { lock (_lock) { - count = _deleted.Count; - sql = _generator.GenerateDeleteSql("TtsChatVoice", _deleted, ["userId", "chatterId"]); + list = _deleted.ToImmutableList(); _deleted.Clear(); } - _logger.Debug($"TtsChatVoice - Deleting {count} rows from database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + count = list.Count; + sql = _generator.GeneratePreparedDeleteSql("TtsChatVoice", count, ["userId", "chatterId"]); + + _logger.Debug($"User - Deleting {count} rows from database: {sql}"); + await _generator.DoPreparedStatement(_database, sql, list, ["id"]); } - return true; } } } \ No newline at end of file diff --git a/Store/GroupSaveSqlGenerator.cs b/Store/GroupSaveSqlGenerator.cs index 6bfe991..a4064c6 100644 --- a/Store/GroupSaveSqlGenerator.cs +++ b/Store/GroupSaveSqlGenerator.cs @@ -1,15 +1,18 @@ using System.Reflection; using System.Text; +using HermesSocketLibrary.db; namespace HermesSocketServer.Store { public class GroupSaveSqlGenerator { private readonly IDictionary columnPropertyRelations; + private readonly Serilog.ILogger _logger; - public GroupSaveSqlGenerator(IDictionary columnsToProperties) + public GroupSaveSqlGenerator(IDictionary columnsToProperties, Serilog.ILogger logger) { columnPropertyRelations = columnsToProperties.ToDictionary(p => p.Key, p => typeof(T).GetProperty(p.Value)); + _logger = logger; var nullProperties = columnPropertyRelations.Where(p => p.Value == null) .Select(p => columnsToProperties[p.Key]); @@ -17,6 +20,24 @@ namespace HermesSocketServer.Store throw new ArgumentException("Some properties do not exist on the values given: " + string.Join(", ", nullProperties)); } + public async Task DoPreparedStatement(Database database, string sql, IEnumerable values, string[] columns) + { + await database.Execute(sql, (c) => + { + var valueCounter = 0; + foreach (var value in values) + { + foreach (var column in columns) + { + var propValue = columnPropertyRelations[column]!.GetValue(value); + var propType = columnPropertyRelations[column]!.PropertyType; + c.Parameters.AddWithValue(column.ToLower() + valueCounter, propValue ?? DBNull.Value); + } + valueCounter++; + } + }); + } + public string GenerateInsertSql(string table, IEnumerable values, IEnumerable columns) { if (string.IsNullOrWhiteSpace(table)) @@ -40,11 +61,42 @@ namespace HermesSocketServer.Store { var propValue = columnPropertyRelations[column]!.GetValue(value); var propType = columnPropertyRelations[column]!.PropertyType; - WriteValue(sb, propValue, propType); + WriteValue(sb, propValue ?? DBNull.Value, propType); sb.Append(","); } sb.Remove(sb.Length - 1, 1) - .Append("),"); + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append(';'); + return sb.ToString(); + } + + public string GeneratePreparedInsertSql(string table, int rows, IEnumerable columns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (columns == null) + throw new ArgumentNullException(nameof(columns)); + if (!columns.Any()) + throw new ArgumentException("Empty list given.", nameof(columns)); + + var ctp = columns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + var columnsLower = columns.Select(c => c.ToLower()); + sb.Append($"INSERT INTO \"{table}\" (\"{string.Join("\", \"", columns)}\") VALUES "); + for (var row = 0; row < rows; row++) + { + sb.Append("("); + foreach (var column in columnsLower) + { + sb.Append('@') + .Append(column) + .Append(row) + .Append(", "); + } + sb.Remove(sb.Length - 2, 2) + .Append("),"); } sb.Remove(sb.Length - 1, 1) .Append(';'); @@ -93,6 +145,44 @@ namespace HermesSocketServer.Store return sb.ToString(); } + public string GeneratePreparedUpdateSql(string table, int rows, IEnumerable keyColumns, IEnumerable updateColumns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (keyColumns == null) + throw new ArgumentNullException(nameof(keyColumns)); + if (!keyColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(keyColumns)); + if (updateColumns == null) + throw new ArgumentNullException(nameof(updateColumns)); + if (!updateColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(updateColumns)); + + var columns = keyColumns.Union(updateColumns); + var ctp = columns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + sb.Append($"UPDATE \"{table}\" as t SET {string.Join(", ", updateColumns.Select(c => "\"" + c + "\" = c.\"" + c + "\""))} FROM (VALUES "); + for (var row = 0; row < rows; row++) + { + sb.Append("("); + foreach (var column in columns) + { + sb.Append('@') + .Append(column) + .Append(row) + .Append(", "); + } + sb.Remove(sb.Length - 2, 2) + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append($") AS c(\"{string.Join("\", \"", columns)}\") WHERE ") + .Append(string.Join(" AND ", keyColumns.Select(c => "t.\"" + c + "\" = c.\"" + c + "\""))) + .Append(";"); + + return sb.ToString(); + } + public string GenerateDeleteSql(string table, IEnumerable keys, IEnumerable keyColumns) { if (string.IsNullOrWhiteSpace(table)) @@ -127,6 +217,37 @@ namespace HermesSocketServer.Store return sb.ToString(); } + public string GeneratePreparedDeleteSql(string table, int rows, IEnumerable keyColumns) + { + if (string.IsNullOrWhiteSpace(table)) + throw new ArgumentException("Value is either null or whitespace-filled.", nameof(table)); + if (keyColumns == null) + throw new ArgumentNullException(nameof(keyColumns)); + if (!keyColumns.Any()) + throw new ArgumentException("Empty list given.", nameof(keyColumns)); + + var ctp = keyColumns.ToDictionary(c => c, c => columnPropertyRelations[c]); + var sb = new StringBuilder(); + sb.Append($"DELETE FROM \"{table}\" WHERE (\"{string.Join("\", \"", keyColumns)}\") IN ("); + for (var row = 0; row < rows; row++) + { + sb.Append("("); + foreach (var column in keyColumns) + { + sb.Append('@') + .Append(column) + .Append(row) + .Append(", "); + } + sb.Remove(sb.Length - 2, 2) + .Append("),"); + } + sb.Remove(sb.Length - 1, 1) + .Append(");"); + + return sb.ToString(); + } + private void WriteValue(StringBuilder sb, object? value, Type type) { if (type == typeof(string)) @@ -134,9 +255,9 @@ namespace HermesSocketServer.Store .Append(value) .Append("'"); else if (type == typeof(Guid)) - sb.Append("'") + sb.Append("uuid('") .Append(value?.ToString()) - .Append("'"); + .Append("')"); else if (type == typeof(TimeSpan)) sb.Append(((TimeSpan)value).TotalMilliseconds); else diff --git a/Store/GroupedSaveStore.cs b/Store/GroupedSaveStore.cs index 839bb1c..b97c486 100644 --- a/Store/GroupedSaveStore.cs +++ b/Store/GroupedSaveStore.cs @@ -28,7 +28,7 @@ namespace HermesSocketServer.Store protected abstract void OnInitialAdd(K key, V value); protected abstract void OnInitialModify(K key, V value); protected abstract void OnInitialRemove(K key); - public abstract Task Save(); + public abstract Task Save(); public V? Get(K key) { diff --git a/Store/IStore.cs b/Store/IStore.cs index 97c61ef..92e3622 100644 --- a/Store/IStore.cs +++ b/Store/IStore.cs @@ -7,7 +7,7 @@ namespace HermesSocketServer.Store Task Load(); bool Modify(K? key, Action action); void Remove(K? key); - Task Save(); + Task Save(); bool Set(K? key, V? value); } } \ No newline at end of file diff --git a/Store/PolicyStore.cs b/Store/PolicyStore.cs index 1cc6e53..59e6254 100644 --- a/Store/PolicyStore.cs +++ b/Store/PolicyStore.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HermesSocketLibrary.db; using HermesSocketServer.Models; @@ -26,7 +27,7 @@ namespace HermesSocketServer.Store { "count", "Usage" }, { "timespan", "Span" }, }; - _generator = new GroupSaveSqlGenerator(ctp); + _generator = new GroupSaveSqlGenerator(ctp, _logger); } public override async Task Load() @@ -61,46 +62,53 @@ namespace HermesSocketServer.Store { } - public override async Task Save() + public override async Task Save() { int count = 0; string sql = string.Empty; + ImmutableList? list = null; if (_added.Any()) { lock (_lock) { - count = _added.Count; - sql = _generator.GenerateInsertSql("GroupPermissionPolicy", _added.Select(a => _store[a]), ["id", "userId", "groupId", "path", "count", "timespan"]); + list = _added.ToImmutableList(); _added.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedInsertSql("GroupPermissionPolicy", count, ["id", "userId", "groupId", "path", "count", "timespan"]); _logger.Debug($"GroupPermissionPolicy - Adding {count} rows to database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "userId", "groupId", "path", "count", "timespan"]); } if (_modified.Any()) { lock (_lock) { - count = _modified.Count; - sql = _generator.GenerateUpdateSql("GroupPermissionPolicy", _modified.Select(m => _store[m]), ["id"], ["userId", "groupId", "path", "count", "timespan"]); + list = _modified.ToImmutableList(); _modified.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedUpdateSql("GroupPermissionPolicy", count, ["id"], ["userId", "groupId", "path", "count", "timespan"]); + _logger.Debug($"GroupPermissionPolicy - Modifying {count} rows in database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "userId", "groupId", "path", "count", "timespan"]); } if (_deleted.Any()) { lock (_lock) { - count = _deleted.Count; - sql = _generator.GenerateDeleteSql("GroupPermissionPolicy", _deleted, ["id"]); + list = _deleted.ToImmutableList(); _deleted.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedDeleteSql("GroupPermissionPolicy", count, ["id"]); + _logger.Debug($"GroupPermissionPolicy - Deleting {count} rows from database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + await _generator.DoPreparedStatement(_database, sql, list, ["id"]); } - return true; } } } \ No newline at end of file diff --git a/Store/UserStore.cs b/Store/UserStore.cs index 267f244..b90d855 100644 --- a/Store/UserStore.cs +++ b/Store/UserStore.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HermesSocketLibrary.db; using HermesSocketServer.Models; @@ -23,7 +24,7 @@ namespace HermesSocketServer.Store { "role", "Role" }, { "ttsDefaultVoice", "DefaultVoice" } }; - _generator = new GroupSaveSqlGenerator(ctp); + _generator = new GroupSaveSqlGenerator(ctp, _logger); } public override async Task Load() @@ -56,45 +57,53 @@ namespace HermesSocketServer.Store { } - public override async Task Save() + public override async Task Save() { int count = 0; string sql = string.Empty; + ImmutableList? list = null; if (_added.Any()) { lock (_lock) { - count = _added.Count; - sql = _generator.GenerateInsertSql("User", _added.Select(a => _store[a]), ["id", "name", "email", "role", "ttsDefaultVoice"]); + list = _added.ToImmutableList(); _added.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedInsertSql("User", count, ["id", "name", "email", "role", "ttsDefaultVoice"]); + _logger.Debug($"User - Adding {count} rows to database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_modified.Any()) { lock (_lock) { - count = _modified.Count; - sql = _generator.GenerateUpdateSql("User", _modified.Select(m => _store[m]), ["id"], ["name", "email", "role", "ttsDefaultVoice"]); + list = _modified.ToImmutableList(); _modified.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedUpdateSql("User", count, ["id"], ["name", "email", "role", "ttsDefaultVoice"]); + _logger.Debug($"User - Modifying {count} rows in database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_deleted.Any()) { lock (_lock) { - count = _deleted.Count; - sql = _generator.GenerateDeleteSql("User", _deleted, ["id"]); + list = _deleted.ToImmutableList(); _deleted.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedDeleteSql("User", count, ["id"]); + _logger.Debug($"User - Deleting {count} rows from database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + await _generator.DoPreparedStatement(_database, sql, list, ["id"]); } - return true; } } } \ No newline at end of file diff --git a/Store/VoiceStore.cs b/Store/VoiceStore.cs index 8fbc8b5..e6b4d35 100644 --- a/Store/VoiceStore.cs +++ b/Store/VoiceStore.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using HermesSocketLibrary.db; using HermesSocketServer.Models; using HermesSocketServer.Validators; @@ -25,7 +26,7 @@ namespace HermesSocketServer.Store { "id", "Id" }, { "name", "Name" } }; - _generator = new GroupSaveSqlGenerator(ctp); + _generator = new GroupSaveSqlGenerator(ctp, _logger); } public override async Task Load() @@ -58,46 +59,53 @@ namespace HermesSocketServer.Store { } - public override async Task Save() + public override async Task Save() { int count = 0; string sql = string.Empty; + ImmutableList? list = null; if (_added.Any()) { lock (_lock) { - count = _added.Count; - sql = _generator.GenerateInsertSql("TtsVoice", _added.Select(a => _store[a]), ["id", "name"]); + list = _added.ToImmutableList(); _added.Clear(); } + count = list.Count; + sql = _generator.GeneratePreparedInsertSql("TtsVoice", count, ["id", "name"]); - _logger.Debug($"TtsVoice - Adding {count} rows to database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + _logger.Debug($"User - Adding {count} rows to database: {sql}"); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_modified.Any()) { lock (_lock) { - count = _modified.Count; - sql = _generator.GenerateUpdateSql("TtsVoice", _modified.Select(m => _store[m]), ["id"], ["name"]); + list = _modified.ToImmutableList(); _modified.Clear(); } - _logger.Debug($"TtsVoice - Modifying {count} rows in database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + count = list.Count; + sql = _generator.GeneratePreparedUpdateSql("TtsVoice", count, ["id"], ["name"]); + + _logger.Debug($"User - Modifying {count} rows in database: {sql}"); + var values = list.Select(id => _store[id]).Where(v => v != null); + await _generator.DoPreparedStatement(_database, sql, values, ["id", "name", "email", "role", "ttsDefaultVoice"]); } if (_deleted.Any()) { lock (_lock) { - count = _deleted.Count; - sql = _generator.GenerateDeleteSql("TtsVoice", _deleted, ["id"]); + list = _deleted.ToImmutableList(); _deleted.Clear(); } - _logger.Debug($"TtsVoice - Deleting {count} rows from database: {sql}"); - await _database.ExecuteScalarTransaction(sql); + count = list.Count; + sql = _generator.GeneratePreparedDeleteSql("TtsVoice", count, ["id"]); + + _logger.Debug($"User - Deleting {count} rows from database: {sql}"); + await _generator.DoPreparedStatement(_database, sql, list, ["id"]); } - return true; } } } \ No newline at end of file diff --git a/db/Database.cs b/db/Database.cs index b60d428..2b1f9de 100644 --- a/db/Database.cs +++ b/db/Database.cs @@ -69,6 +69,18 @@ namespace HermesSocketLibrary.db return await command.ExecuteNonQueryAsync(); } + public async Task ExecuteTransaction(string sql, Action prepare) + { + await using var connection = await _source.OpenConnectionAsync(); + await using var transaction = await connection.BeginTransactionAsync(); + await using var command = new NpgsqlCommand(sql, connection, transaction); + prepare(command); + await command.PrepareAsync(); + var results = await command.ExecuteNonQueryAsync(); + await transaction.CommitAsync(); + return results; + } + public async Task ExecuteScalar(string sql, IDictionary? values = null) { await using var connection = await _source.OpenConnectionAsync();