443 lines
20 KiB
C#
443 lines
20 KiB
C#
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<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())),
|
|
#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<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() { }
|
|
private object _databaseConnection;
|
|
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 =>
|
|
{
|
|
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<string, object>(key, jsonData));
|
|
}
|
|
else
|
|
{
|
|
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)
|
|
{
|
|
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<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(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<KeyValuePair<string, object>> parametersUpdated)
|
|
{
|
|
var errors = new List<string>();
|
|
IEnumerable<string> textParams = Regex.Matches(funcText, @"(:\w+)").Cast<Match>().Select(x => x.Value.ToLower()).Distinct().ToList();
|
|
var paramParsedList = new List<SQLiteFunctionParam>();
|
|
|
|
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<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 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<string>("FuncName");
|
|
funcParams = readerD.Get<string>("FuncParams");
|
|
funcText = readerD.Get<string>("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<TResult>(RequestParams requestParams, Func<SqliteCommand, TResult> 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<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();
|
|
}
|
|
readerSpecified.ProcessExtraSelect(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);
|
|
}
|
|
} |