408 lines
18 KiB
C#
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);
|
|
} |