using Microsoft.Data.Sqlite; using Npgsql.NameTranslation; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.RegularExpressions; namespace Kit.Helpers.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()!)), new ConverterItem(typeof(DateOnly), typeof(string), (value) => DateOnly.ParseExact(value.ToString()!, "yyyy-MM-dd")), 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()!), new ConverterItem(typeof(TimeOnly), typeof(string), (value) => TimeOnly.ParseExact(value.ToString()!, "HH:mm:ss.fffffff", CultureInfo.InvariantCulture)), 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() { } 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 => { 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) { var conn = new SqliteConnection(fileName.Value.CreateSQLiteConnectionString()); conn.Open(); return conn; } private Func _funcStrToIntJsonArr = (string input, string delimiter) => { if (string.IsNullOrWhiteSpace(input)) { return new List().ToJSON(); } if (string.IsNullOrWhiteSpace(delimiter)) { delimiter = ","; } HashSet ints = input.Split(delimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Select(x => int.TryParse(x.Trim(), out int result) ? result : 0).ToHashSet(); return ints.ToJSON(); }; private class SQLiteProcedureParam { public string Name { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; public bool IsOptional { get; set; } [MemberNotNullWhen(true, "DefaultValueParsed")] 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 procedureName, string procedureParams, string procedureText, List> parametersUpdated) { var errors = new List(); IEnumerable textParams = Regex.Matches(procedureText, @"(:\w+)").Select(x => x.Value.ToLower()).Distinct().ToList(); var paramParsedList = new List(); procedureParams.Split(",", StringSplitOptions.TrimEntries).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 SQLiteProcedureParam(); 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 GetStoredProcedureContent(SqliteCommand command, string input, out string procedureName, out string procedureParams, out string procedureText) { string oldText = command.CommandText; CommandType oldType = command.CommandType; command.CommandType = CommandType.Text; command.CommandText = "select procedure_name, params, text from procedures where procedure_name = :procedure_name"; command.Parameters.AddWithValue(":procedure_name", input); using (var reader = command.ExecuteReader()) { if (reader.Read() == false) { throw new Exception($"procedure '{input}' not found"); } var readerD = new SQLiteDataRecordSpecified(reader, Translator); procedureName = readerD.Get("ProcedureName"); procedureParams = readerD.Get("Params"); procedureText = readerD.Get("Text"); if (procedureName != input) { throw new Exception($"invalid procedure name (requested: {input}; received: {procedureName})"); } } 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) { GetStoredProcedureContent(command, sql, out string procedureName, out string procedureParams, out string procedureText); ValidateAndCompleteParams(procedureName, procedureParams, procedureText, parametersUpdated); sql = procedureText; } 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 ? Guid.Empty : DBNull.Value)); } } return command; } private TResult ExecuteOnConnect(RequestParams requestParams, Func func) { return _lockService.Lock(requestParams.ConnectionString.Value, () => { bool needNewConnect = requestParams.ConnectionString.DatabaseConnection == null || requestParams.ConnectionString.DatabaseConnection is SqliteConnection == false; SqliteConnection connection = needNewConnect ? CreateConnection(requestParams.ConnectionString) : (SqliteConnection)requestParams.ConnectionString.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(); } reader.ProcessExtraSelect(Translator, 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); }