using Npgsql; using Npgsql.NameTranslation; using System.Data; using System.Text; namespace Kit.Helpers.Postgres; public class SqlHelper : ISqlHelper { private SqlHelper() { } public INpgsqlNameTranslator Translator { get; } = new NpgsqlSnakeCaseNameTranslator(); public static SqlHelper Instance { get; } = new SqlHelper(); private IEnumerable>? UpdateParamNames(IEnumerable>? @params) { var result = new List>(); if (@params.IsNullOrEmpty()) return result; @params.ForEach(_param => { string key = "_" + Translator.TranslateMemberName(_param.Key.Remove(" ")!.Trim('@', '_')); result.Add(new KeyValuePair(key, _param.Value)); }); return result; } private string GenerateParamsForQuery(IEnumerable>? @params) { var sb = new StringBuilder(); if (@params.IsNullOrEmpty() == false) { for (int i = 0; i < @params.Count(); i++) { string key = @params.ElementAt(i).Key; sb.Append(key).Append(" => :").Append(key); if (i < @params.Count() - 1) { sb.Append(", "); } } } return sb.ToString(); } private string TranslateName(string name, bool withStrictSyntax) { if (withStrictSyntax) { return name; } return name.Remove("[")!.Remove("]")!.Split('.').Select(x => Translator.TranslateMemberName(x)).Join("."); } private NpgsqlCommand AppendTypes(NpgsqlCommand command, RequestParams requestParams) { requestParams.CustomTypes?.ForEach(x => { command.Connection?.TypeMapper.MapComposite(x.Type, TranslateName(x.Name, requestParams.WithStrictSyntax), Translator); }); return command; } private NpgsqlCommand CreateCommand(RequestParams requestParams) { string sql = TranslateName(requestParams.CommandText, requestParams.WithStrictSyntax); bool hasParameters = requestParams.Parameters != null && requestParams.Parameters.Count() > 0; var parameters = UpdateParamNames(requestParams.Parameters); if (requestParams.ConnectionString.Value.IsNullOrEmpty()) throw new ArgumentNullException("connectionString"); if (sql.IsNullOrEmpty()) throw new ArgumentNullException("sql"); var connection = new NpgsqlConnection(requestParams.ConnectionString.Value); NpgsqlCommand command = connection.CreateCommand(); command.CommandTimeout = connection.ConnectionTimeout; if (requestParams.IsStoredProcedure) { sql = $"select * from {sql}({GenerateParamsForQuery(parameters)});"; } command.CommandType = System.Data.CommandType.Text; command.CommandText = sql; if (parameters != null && parameters.Any()) { foreach (var keyValuePair in parameters) { string key = keyValuePair.Key.Remove("@").Remove(" ").ToLower(); if (keyValuePair.Value is Guid) { command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? Guid.Empty); } else { command.Parameters.AddWithValue(key.ToLower(), keyValuePair.Value ?? DBNull.Value); } } } return command; } public void ExecuteNonQuery(RequestParams requestParams) { NpgsqlCommand command = CreateCommand(requestParams); using (command.Connection) { command.Connection.Open(); AppendTypes(command, requestParams); command.ExecuteNonQuery(); command.Connection.Close(); } } public object ExecuteScalar(RequestParams requestParams) { object returnScalar; NpgsqlCommand command = CreateCommand(requestParams); using (command.Connection) { command.Connection.Open(); AppendTypes(command, requestParams); returnScalar = command.ExecuteScalar(); command.Connection.Close(); } return returnScalar; } public TResult ExecuteScalar(RequestParams requestParams) { object returnScalar; NpgsqlCommand command = CreateCommand(requestParams); using (command.Connection) { command.Connection.Open(); AppendTypes(command, requestParams); returnScalar = command.ExecuteScalar(); command.Connection.Close(); } return (TResult)returnScalar; } private IEnumerable ExecuteSelectMany(SelectType extraSelectType, RequestParamsSelect requestParams) { if (requestParams.Converters == null || requestParams.Converters.Count() < 1) throw new ArgumentNullException("converters"); NpgsqlCommand command = CreateCommand(requestParams); command.CommandTimeout = command.Connection.ConnectionTimeout; IList selectResult = extraSelectType.CreateList(); using (command.Connection) { command.Connection.Open(); AppendTypes(command, requestParams); using (NpgsqlTransaction tran = command.Connection.BeginTransaction()) { using (IDataReader readerFirst = command.ExecuteReader()) { var dataTypeName = readerFirst.GetDataTypeName(0); // Для первого столбца if (dataTypeName == "refcursor") { ProcessForCursors(readerFirst, command, requestParams.Converters, selectResult, extraSelectType); } else { ProcessForSimple(readerFirst, requestParams.Converters, selectResult, extraSelectType); } } tran.Commit(); } } return selectResult; } private void ProcessForSimple(IDataReader reader, IEnumerable>> converters, IList selectResult, SelectType extraSelectType = SelectType.Default) { IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator); foreach (var converter in converters) { while (reader.Read()) { converter(recordSpecified, selectResult); } reader.NextResult(); } reader.ProcessExtraSelect(Translator, selectResult, extraSelectType); } private void CheckCursorCount(int cursorCount, int converterCount, SelectType extraSelectType) { switch (extraSelectType) { case SelectType.Page: case SelectType.Offer: Check.IsTrue(cursorCount >= converterCount + 1, "Количество конвертеров больше количества возвращаемых курсоров"); break; case SelectType.Default: default: Check.IsTrue(cursorCount >= converterCount, "Количество конвертеров больше количества возвращаемых курсоров"); break; } } private void ProcessForCursors(IDataReader readerFirst, NpgsqlCommand command, IEnumerable>> converters, IList selectResult, SelectType extraSelectType = SelectType.Default) { var cursors = new List(); while (readerFirst.Read()) { cursors.Add(readerFirst[0].ToString()!); } readerFirst.Close(); CheckCursorCount(cursors.Count, converters.Count(), extraSelectType); for (int i = 0; i < converters.Count(); i++) { command.CommandText = $"fetch all in \"{cursors[i]}\""; command.CommandType = CommandType.Text; using (NpgsqlDataReader reader = command.ExecuteReader()) { IDataRecordSpecified recordSpecified = new DataRecordSpecified(reader, Translator); while (reader.Read()) { converters.ElementAt(i)(recordSpecified, selectResult); } } } if (extraSelectType != SelectType.Default) { // считывание данных постраничного вывода command.CommandText = $"fetch all in \"{cursors.Last()}\""; command.CommandType = CommandType.Text; using (NpgsqlDataReader reader = command.ExecuteReader()) { reader.ProcessExtraSelect(Translator, selectResult, extraSelectType); } } } public IEnumerable ExecuteSelectMany(RequestParamsSelect requestParams) => ExecuteSelectMany(SelectType.Default, requestParams); public IEnumerableWithPage ExecuteSelectManyWithPage(RequestParamsSelect requestParams) => (ListWithPage)ExecuteSelectMany(SelectType.Page, requestParams); public IEnumerableWithOffer ExecuteSelectManyWithOffer(RequestParamsSelect requestParams) => (ListWithOffer)ExecuteSelectMany(SelectType.Offer, requestParams); } public static class SqlHelperExtensions { public static TValue Get(this IDataRecordSpecified record, string fieldName) => record.Get(fieldName); public static TValue GetN(this IDataRecordSpecified record, string fieldName) => record.GetN(fieldName); }