Kit.Core/LibCommon/Kit.Core.Helpers/Db/SqlHelperSQLite.cs

408 lines
18 KiB
C#

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<TValue>(object value) => SQliteConvert.Convert<TValue>(value);
}
internal static class SQliteConvert
{
private static readonly IEnumerable<ConverterItem> _typeConverters = new List<ConverterItem>
{
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<object, object> Func { get; set; }
public ConverterItem(Type tgt, Type src, Func<object, object> func)
{
this.Tgt = tgt;
this.Src = src;
this.Func = func;
}
}
public static TValue Convert<TValue>(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<string> _lockService = new LockService<string>();
private SqlHelper() { }
public Npgsql.INpgsqlNameTranslator Translator { get; } = new NpgsqlSnakeCaseNameTranslator();
public static SqlHelper Instance { get; } = new SqlHelper();
private List<KeyValuePair<string, object>> UpdateParamNames(RequestParams requestParams)
{
var result = new List<KeyValuePair<string, object>>();
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<string, object>(key, _param.Value));
});
return result;
}
private SqliteConnection CreateConnection(IConnectionString fileName)
{
var conn = new SqliteConnection(fileName.Value.CreateSQLiteConnectionString());
conn.Open();
return conn;
}
private Func<string, string, string> _funcStrToIntJsonArr = (string input, string delimiter) =>
{
if (string.IsNullOrWhiteSpace(input))
{
return new List<int>().ToJSON();
}
if (string.IsNullOrWhiteSpace(delimiter))
{
delimiter = ",";
}
HashSet<int> 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<KeyValuePair<string, object>> parametersUpdated)
{
var errors = new List<string>();
IEnumerable<string> textParams = Regex.Matches(procedureText, @"(:\w+)").Select(x => x.Value.ToLower()).Distinct().ToList();
var paramParsedList = new List<SQLiteProcedureParam>();
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<SqliteType>(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<string> 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<string> 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<string> 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<string, object>(x.Name, x.DefaultValueParsed));
}
else
{
parametersUpdated.Add(new KeyValuePair<string, object>(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<string>("ProcedureName");
procedureParams = readerD.Get<string>("Params");
procedureText = readerD.Get<string>("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<TResult>(RequestParams requestParams, Func<SqliteCommand, TResult> 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<TResult>(RequestParams requestParams)
{
return ExecuteOnConnect(requestParams, (SqliteCommand command) => SQliteConvert.Convert<TResult>(command.ExecuteScalar()!));
}
private IEnumerable<TEntity> ExecuteSelectMany<TEntity>(SelectType extraSelectType, RequestParamsSelect<TEntity> requestParams)
{
if (requestParams.Converters == null || requestParams.Converters.Count() < 1) throw new ArgumentNullException("converters");
return ExecuteOnConnect(requestParams, (SqliteCommand command) =>
{
IList<TEntity> selectResult = extraSelectType.CreateList<TEntity>();
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<TEntity> ExecuteSelectMany<TEntity>(RequestParamsSelect<TEntity> requestParams) => ExecuteSelectMany(SelectType.Default, requestParams);
public IEnumerableWithPage<TEntity> ExecuteSelectManyWithPage<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithPage<TEntity>)ExecuteSelectMany(SelectType.Page, requestParams);
public IEnumerableWithOffer<TEntity> ExecuteSelectManyWithOffer<TEntity>(RequestParamsSelect<TEntity> requestParams) => (ListWithOffer<TEntity>)ExecuteSelectMany(SelectType.Offer, requestParams);
}