using Microsoft.Data.Sqlite; using Npgsql.NameTranslation; using System; using System.Collections.Generic; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; namespace Kit.Helpers.SqlHelperAbstract.SQLite { internal class SQLiteDataRecordSpecified : DataRecordSpecified { public SQLiteDataRecordSpecified(IDataReader dataReader, Npgsql.INpgsqlNameTranslator nameTranslator) : base(dataReader, nameTranslator) { } public override TValue Convert(object value) => SQliteConvert.Convert(value); } internal static class SQliteConvert { private static readonly IEnumerable _typeConverters = new List { new ConverterItem(typeof(bool), typeof(long), (value) => System.Convert.ToBoolean(value)), new ConverterItem(typeof(byte), typeof(long), (value) => System.Convert.ToByte(value)), new ConverterItem(typeof(char), typeof(string), (value) => System.Convert.ToChar(value.ToString())), #if NETCOREAPP new ConverterItem(typeof(DateOnly), typeof(string), (value) => DateOnly.ParseExact(value.ToString(), "yyyy-MM-dd")), #endif new ConverterItem(typeof(DateTime), typeof(string), (value) => DateTime.ParseExact(value.ToString(), "yyyy-MM-dd HH:mm:ss.FFFFFFF", CultureInfo.InvariantCulture)), new ConverterItem(typeof(DateTimeOffset), typeof(string), (value) => DateTimeOffset.ParseExact(value.ToString(), "yyyy-MM-dd HH:mm:ss.FFFFFFFzzz", CultureInfo.InvariantCulture)), new ConverterItem(typeof(decimal), typeof(string), (value) => System.Convert.ToDecimal(value.ToString())), new ConverterItem(typeof(double), typeof(double), (value) => value), new ConverterItem(typeof(Guid), typeof(string), (value) => Guid.Parse(value.ToString())), new ConverterItem(typeof(short), typeof(long), (value) => System.Convert.ToInt16(value)), new ConverterItem(typeof(int), typeof(long), (value) => System.Convert.ToInt32(value)), new ConverterItem(typeof(long), typeof(long), (value) => value), new ConverterItem(typeof(sbyte), typeof(long), (value) => System.Convert.ToSByte(value)), new ConverterItem(typeof(float), typeof(double), (value) => System.Convert.ToSingle(value)), new ConverterItem(typeof(string), typeof(string), (value) => value.ToString()), #if NETCOREAPP new ConverterItem(typeof(TimeOnly), typeof(string), (value) => TimeOnly.ParseExact(value.ToString(), "HH:mm:ss.fffffff", CultureInfo.InvariantCulture)), #endif new ConverterItem(typeof(TimeSpan), typeof(string), (value) => TimeSpan.ParseExact(value.ToString(), "d.hh:mm:ss.fffffff", CultureInfo.InvariantCulture)), new ConverterItem(typeof(ushort), typeof(long), (value) => System.Convert.ToUInt16(value)), new ConverterItem(typeof(uint), typeof(long), (value) => System.Convert.ToUInt32(value)), new ConverterItem(typeof(ulong), typeof(long), (value) => System.Convert.ToUInt64(value)), }; private class ConverterItem { public Type Tgt { get; set; } public Type Src { get; set; } public Func Func { get; set; } public ConverterItem(Type tgt, Type src, Func func) { this.Tgt = tgt; this.Src = src; this.Func = func; } } public static TValue Convert(object value) { Type valueType = value.GetType(); Type targetType = typeof(TValue); return (TValue)(_typeConverters.FirstOrDefault(x => x.Tgt == targetType && x.Src == valueType)?.Func(value) ?? value); } } public class SqlHelper : ISqlHelper { private readonly ILockService _lockService = new LockService(); private SqlHelper() { } private object _databaseConnection; public Npgsql.INpgsqlNameTranslator Translator { get; } = new NpgsqlSnakeCaseNameTranslator(); public static SqlHelper Instance { get; } = new SqlHelper(); private List> UpdateParamNames(RequestParams requestParams) { var result = new List>(); if (requestParams.Parameters.IsNullOrEmpty()) return result; requestParams.Parameters.ForEach(_param => { if ((_param.Value.GetType().IsGenericType && _param.Value.GetType().GetGenericTypeDefinition() == typeof(List<>)) || (_param.Value.GetType() == typeof(int[])) ) { var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; string jsonData = JsonSerializer.Serialize(_param.Value, jsonOptions); string key = ":" + (requestParams.WithStrictSyntax ? _param.Key : Translator.TranslateMemberName(_param.Key.Remove(" ").Trim('@', '_'))); result.Add(new KeyValuePair(key, jsonData)); } else { string key = ":" + (requestParams.WithStrictSyntax ? _param.Key : Translator.TranslateMemberName(_param.Key.Remove(" ").Trim('@', '_'))); result.Add(new KeyValuePair(key, _param.Value)); } }); return result; } private SqliteConnection CreateConnection(IConnectionString fileName) { string connectionString = ""; try { connectionString = fileName.Value.CreateSQLiteConnectionString(); var conn = new SqliteConnection(connectionString); conn.Open(); return conn; } catch (Exception ex) { throw new Exception($"ConnectionString:{connectionString} Message:{ex.Message}", ex); } } private Func _funcStrToIntJsonArr = (string input, string delimiter) => { if (string.IsNullOrWhiteSpace(input)) { return new List().ToJSON(); } if (string.IsNullOrWhiteSpace(delimiter)) { delimiter = ","; } HashSet ints = input.Split(new string[] { delimiter }, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).Select(x => int.TryParse(x.Trim(), out int result) ? result : 0).ToHashSet(); return ints.ToJSON(); }; private class SQLiteFunctionParam { public string Name { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; public bool IsOptional { get; set; } #if NETCOREAPP [MemberNotNullWhen(true, "DefaultValueParsed")] #endif public bool HasDefault { get; set; } public string DefaultValue { get; set; } = string.Empty; public SqliteType TypeParsed { get; set; } = SqliteType.Blob; public object DefaultValueParsed { get; set; } } private void ValidateAndCompleteParams(string funcName, string funcParams, string funcText, List> parametersUpdated) { var errors = new List(); IEnumerable textParams = Regex.Matches(funcText, @"(:\w+)").Cast().Select(x => x.Value.ToLower()).Distinct().ToList(); var paramParsedList = new List(); funcParams.Split(',').Select(x => x.Trim()).ForEach(x => { // Обязательный, без значения по умолчанию var regexReq = new Regex(@"^(:\w+)\s+(\S+)$"); // Необязательный, по умолчанию null var regexOpt = new Regex(@"^(:\w+)\s+(\S+)(?i:\s*=\s*|\s+default\s+)(?i:null)$"); // Необязательный, указано значение по умолчанию var regexOptW = new Regex(@"^(:\w+)\s+(\S+)(?i:\s*=\s*|\s+default\s+)(.*)"); Match match = null; var param = new SQLiteFunctionParam(); if ((match = regexReq.Match(x)).Success) { param.Name = match.Groups[1].Value; param.Type = match.Groups[2].Value; param.IsOptional = false; param.HasDefault = false; param.DefaultValue = string.Empty; } else if ((match = regexOpt.Match(x)).Success) { param.Name = match.Groups[1].Value; param.Type = match.Groups[2].Value; param.IsOptional = true; param.HasDefault = false; param.DefaultValue = string.Empty; } else if ((match = regexOptW.Match(x)).Success) { param.Name = match.Groups[1].Value; param.Type = match.Groups[2].Value; param.IsOptional = true; param.HasDefault = true; param.DefaultValue = match.Groups[3].Value; } else { errors.Add($"Invalid parameter: {x}"); return; } param.Name = param.Name.ToLower(); try { param.TypeParsed = param.Type.ParseToEnum(ignoreCase: true); } catch { errors.Add($"parameter '{param.Name}': invalid type ({param.Type})"); return; } try { if (param.HasDefault) { switch (param.TypeParsed) { case SqliteType.Integer: param.DefaultValueParsed = int.Parse(param.DefaultValue); break; case SqliteType.Real: param.DefaultValueParsed = double.Parse(param.DefaultValue); break; case SqliteType.Text: if (param.DefaultValue.StartsWith("\'") == false || param.DefaultValue.EndsWith("\'") == false) throw new Exception(); param.DefaultValueParsed = param.DefaultValue.Trim('\'').Replace("''", "'"); if (((string)param.DefaultValueParsed).IndexOf("''") != -1) throw new Exception(); break; case SqliteType.Blob: default: break; } } } catch { errors.Add($"parameter '{param.Name}': invalid default value ({param.DefaultValue})"); param.DefaultValueParsed = null; return; } paramParsedList.Add(param); }); IEnumerable delacrationDuplicates = paramParsedList.GroupBy(x => x.Name.ToLower()).Where(x => x.Count() > 1).Select(x => x.Key).ToList(); if (delacrationDuplicates.Count() > 0) { errors.AddRange(delacrationDuplicates.Select(x => $"There are duplicates of the parameter declaration: {x}")); } IEnumerable notDescribedParams = textParams.Where(x => paramParsedList.Any(y => y.Name == x) == false).ToList(); if (notDescribedParams.Count() > 0) { errors.AddRange(notDescribedParams.Select(x => $"parameter not declared: {x}")); } if (errors.Count > 0) { throw new Exception(errors.Join(Environment.NewLine)); } // Ловим неуказанные обязательные параметры IEnumerable notSendedRequired = paramParsedList.Where(x => x.IsOptional == false && parametersUpdated.Any(y => y.Key == x.Name) == false).Select(x => x.Name).ToList(); if (notSendedRequired.Count() > 0) { errors.AddRange(notSendedRequired.Select(x => $"required parameter is not sended: {x}")); } if (errors.Count > 0) { throw new Exception(errors.Join(Environment.NewLine)); } // Ловим неуказанные необязательные параметры paramParsedList.Where(x => x.IsOptional && parametersUpdated.Any(y => y.Key == x.Name) == false).ForEach(x => { if (x.HasDefault) { parametersUpdated.Add(new KeyValuePair(x.Name, x.DefaultValueParsed)); } else { parametersUpdated.Add(new KeyValuePair(x.Name, DBNull.Value)); } }); } private void GetStoredFunctionContent(SqliteCommand command, string input, out string funcName, out string funcParams, out string funcText) { string oldText = command.CommandText; CommandType oldType = command.CommandType; command.CommandType = CommandType.Text; command.CommandText = SqlTexts.SelectStoredFunction; command.Parameters.AddWithValue(":func_name", input); using (var reader = command.ExecuteReader()) { if (reader.Read() == false) { throw new Exception($"function '{input}' not found"); } var readerD = new SQLiteDataRecordSpecified(reader, Translator); funcName = readerD.Get("FuncName"); funcParams = readerD.Get("FuncParams"); funcText = readerD.Get("FuncText"); if (funcName != input) { throw new Exception($"invalid function name (requested: {input}; received: {funcName})"); } } command.CommandText = oldText; command.CommandType = oldType; command.Parameters.Clear(); } private string GetCommandText(RequestParams requestParams) { if (requestParams.WithStrictSyntax) { return requestParams.CommandText; } return requestParams.CommandText.Remove("[").Remove("]").Split('.').Select(x => Translator.TranslateMemberName(x)).Join("_"); } private SqliteCommand CreateCommand(SqliteConnection connection, RequestParams requestParams) { if (requestParams.CommandText.IsNullOrEmpty()) throw new ArgumentNullException("sql"); string sql = GetCommandText(requestParams); var parametersUpdated = UpdateParamNames(requestParams); SqliteCommand command = connection.CreateCommand(); command.CommandTimeout = connection.ConnectionTimeout; // Регистрируем пользовательские функции connection.CreateFunction("str_to_int_json_arr", _funcStrToIntJsonArr, true); if (requestParams.IsStoredProcedure) { GetStoredFunctionContent(command, sql, out string funcName, out string funcParams, out string funcText); ValidateAndCompleteParams(funcName, funcParams, funcText, parametersUpdated); sql = funcText; } if (sql.IsNullOrEmpty()) throw new ArgumentNullException("sql"); command.CommandType = CommandType.Text; command.CommandText = sql; if (parametersUpdated.Count > 0) { foreach (var keyValuePair in parametersUpdated) { command.Parameters.AddWithValue(keyValuePair.Key, keyValuePair.Value ?? (keyValuePair.Value is Guid ? (object)Guid.Empty : (object)DBNull.Value)); } } return command; } private TResult ExecuteOnConnect(RequestParams requestParams, Func func) { return _lockService.Lock(requestParams.ConnectionString.Value, () => { bool needNewConnect = _databaseConnection == null || _databaseConnection is SqliteConnection == false; SqliteConnection connection = needNewConnect ? CreateConnection(requestParams.ConnectionString) : (SqliteConnection)_databaseConnection; try { using (SqliteCommand command = CreateCommand(connection, requestParams)) return func(command); } finally { if (needNewConnect) connection?.Dispose(); connection = null; } }); } public void ExecuteNonQuery(RequestParams requestParams) { ExecuteOnConnect(requestParams, (SqliteCommand command) => command.ExecuteNonQuery()); } public object ExecuteScalar(RequestParams requestParams) { return ExecuteOnConnect(requestParams, (SqliteCommand command) => command.ExecuteScalar()); } public TResult ExecuteScalar(RequestParams requestParams) { return ExecuteOnConnect(requestParams, (SqliteCommand command) => SQliteConvert.Convert(command.ExecuteScalar())); } private IEnumerable ExecuteSelectMany(SelectType extraSelectType, RequestParamsSelect requestParams) { if (requestParams.Converters == null || requestParams.Converters.Count() < 1) throw new ArgumentNullException("converters"); return ExecuteOnConnect(requestParams, (SqliteCommand command) => { IList selectResult = extraSelectType.CreateList(); using (IDataReader reader = command.ExecuteReader()) { IDataRecordSpecified readerSpecified = new SQLiteDataRecordSpecified(reader, Translator); foreach (var converter in requestParams.Converters) { while (reader.Read()) { converter(readerSpecified, selectResult); } reader.NextResult(); } readerSpecified.ProcessExtraSelect(selectResult, extraSelectType); } return selectResult; }); } 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); } }