| | | 1 | | using System.Linq.Expressions; |
| | | 2 | | using System.Reflection; |
| | | 3 | | using Microsoft.EntityFrameworkCore; |
| | | 4 | | using OzonDomains.Models; |
| | | 5 | | using static OzonOrdersWeb.WebServices.Orders.Grid.OrdersGridFormula; |
| | | 6 | | |
| | | 7 | | namespace OzonOrdersWeb.WebServices.Orders.Grid; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// Перевод формулы вычисляемого столбца в EF-выражение над <see cref="Order"/> — чтобы по столбцу можно было |
| | | 11 | | /// сортировать, фильтровать и считать счётчики значений на сервере. Переводится подмножество: поля, лежащие в |
| | | 12 | | /// таблице Orders или в справочниках (JOIN), арифметика, сравнения, если/первоеНепустое/мин/макс, текстовые и |
| | | 13 | | /// датные функции с прямым SQL-аналогом. Семантика «пусто → пусто» совпадает с NULL в SQL. |
| | | 14 | | /// Непереводимое (поля, считаемые в памяти; текст(число) и т.п.) — <see cref="NotTranslatableException"/>. |
| | | 15 | | /// </summary> |
| | | 16 | | public static class OrdersGridFormulaTranslator |
| | | 17 | | { |
| | 4 | 18 | | public sealed class NotTranslatableException(string message) : Exception(message); |
| | | 19 | | |
| | | 20 | | /// <summary>Готовое выражение: лямбда Order → T (T = decimal?/string/DateTime?/bool?) и тип результата формулы.</su |
| | 69 | 21 | | public sealed record Translation(LambdaExpression Lambda, string Type) |
| | | 22 | | { |
| | 0 | 23 | | public Type ClrType => Lambda.ReturnType; |
| | | 24 | | } |
| | | 25 | | |
| | 1 | 26 | | private static readonly ParameterExpression Param = Expression.Parameter(typeof(Order), "o"); |
| | | 27 | | |
| | | 28 | | /// <summary>Поля, у которых есть SQL-представление. Остальные из каталога считаются в памяти и не переводятся.</sum |
| | 1 | 29 | | private static readonly Dictionary<string, Func<Expression>> FieldExpressions = new(StringComparer.OrdinalIgnoreCase |
| | 1 | 30 | | { |
| | 0 | 31 | | ["shipmentNumber"] = () => Prop("ShipmentNumber"), |
| | 0 | 32 | | ["ozonClient"] = () => NavName("OzonClient"), |
| | 0 | 33 | | ["deliveryCity"] = () => Prop("DeliveryCity"), |
| | 5 | 34 | | ["processingDate"] = () => Prop("ProcessingDate"), |
| | 3 | 35 | | ["shippingDate"] = () => Prop("ShippingDate"), |
| | 0 | 36 | | ["lastStatusChangeDate"] = () => Prop("LastStatusChangeDate"), |
| | 0 | 37 | | ["productKey"] = () => Prop("ProductKey"), |
| | 3 | 38 | | ["manufacturer"] = () => NavName("EtProducer"), |
| | 3 | 39 | | ["article"] = () => Prop("Article"), |
| | 1 | 40 | | ["productName"] = () => Prop("ProductName"), |
| | 0 | 41 | | ["category"] = () => NavProp("ProductInfo", "CommercialCategory"), |
| | 0 | 42 | | ["volumetricWeight"] = () => ToDecimal(NavProp("ProductInfo", "VolumetricWeight")), |
| | 0 | 43 | | ["weight"] = () => NavProp("ProductInfo", "Weight"), |
| | 0 | 44 | | ["clientStatus"] = () => NavName("ClientStatus"), |
| | 1 | 45 | | ["appStatus"] = () => NavName("AppStatus"), |
| | 0 | 46 | | ["shipmentWarehouse"] = () => NavName("ShipmentWarehouse"), |
| | 0 | 47 | | ["deliveryProvider"] = () => NavNavName("Delivery", "Provider"), |
| | 0 | 48 | | ["deliveryName"] = () => NavName("Delivery"), |
| | 2 | 49 | | ["trackingNumber"] = () => Prop("TrackingNumber"), |
| | 1 | 50 | | ["scanit"] = () => Prop("Scanit"), |
| | 1 | 51 | | ["supplier"] = () => NavName("Supplier"), |
| | 1 | 52 | | ["orderNumberToSupplier"] = () => Prop("OrderNumberToSupplier"), |
| | 1 | 53 | | ["sitePrice"] = () => NavProp("ProductInfo", "CurrentPriceWithDiscount"), |
| | 4 | 54 | | ["quantity"] = () => ToDecimal(Prop("Quantity")), |
| | 6 | 55 | | ["price"] = () => Prop("Price"), |
| | 0 | 56 | | ["currency"] = () => NavName("Сurrency"), |
| | 2 | 57 | | ["shipmentAmount"] = () => Prop("ShipmentAmount"), |
| | 0 | 58 | | ["purchasePrice"] = () => Prop("PurchasePrice"), |
| | 0 | 59 | | ["costPrice"] = () => Prop("CostPrice"), |
| | 0 | 60 | | ["ozonCommissionMin"] = () => Prop("MinOzonCommission"), |
| | 0 | 61 | | ["ozonCommissionMax"] = () => Prop("MaxOzonCommission"), |
| | 0 | 62 | | ["profitMin"] = () => Prop("MinProfit"), |
| | 2 | 63 | | ["profitMax"] = () => Prop("MaxProfit"), |
| | 0 | 64 | | ["discountMin"] = () => Prop("MinDiscount"), |
| | 0 | 65 | | ["discountMax"] = () => Prop("MaxDiscount"), |
| | 1 | 66 | | ["isArchived"] = () => ToNullableBool(Expression.NotEqual(Prop("OrderArchive"), Expression.Constant(null))), |
| | 1 | 67 | | ["fromFile"] = () => Prop("FromFile") |
| | 1 | 68 | | }; |
| | | 69 | | |
| | 0 | 70 | | public static bool IsFieldTranslatable(string key) => FieldExpressions.ContainsKey(key); |
| | | 71 | | |
| | | 72 | | /// <summary>Поле по ключу или русскому названию из каталога.</summary> |
| | | 73 | | private static string? ResolveFieldKey(string nameOrLabel) |
| | | 74 | | { |
| | 41 | 75 | | var f = OrdersGridFormulaFields.All.FirstOrDefault(x => |
| | 913 | 76 | | string.Equals(x.Key, nameOrLabel, StringComparison.OrdinalIgnoreCase) || |
| | 913 | 77 | | string.Equals(x.Label, nameOrLabel, StringComparison.OrdinalIgnoreCase)); |
| | 41 | 78 | | return f?.Key; |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | /// <summary>Перевести формулу; бросает <see cref="NotTranslatableException"/> или <see cref="FormulaException"/>.</ |
| | | 82 | | public static Translation Translate(string formula) |
| | | 83 | | { |
| | 27 | 84 | | var node = Parse(formula); |
| | 27 | 85 | | var body = Build(node); |
| | 23 | 86 | | return new Translation(Expression.Lambda(body, Param), TypeOf(body.Type)); |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | public static bool TryTranslate(string formula, out Translation? translation, out string? reason) |
| | | 90 | | { |
| | | 91 | | try |
| | | 92 | | { |
| | 4 | 93 | | translation = Translate(formula); |
| | 0 | 94 | | reason = null; |
| | 0 | 95 | | return true; |
| | | 96 | | } |
| | 16 | 97 | | catch (NotTranslatableException ex) { translation = null; reason = ex.Message; return false; } |
| | 0 | 98 | | catch (FormulaException ex) { translation = null; reason = ex.Message; return false; } |
| | 4 | 99 | | } |
| | | 100 | | |
| | | 101 | | // ---------- Построение ---------- |
| | | 102 | | |
| | 1 | 103 | | private static readonly Type Dec = typeof(decimal?); |
| | 1 | 104 | | private static readonly Type Str = typeof(string); |
| | 1 | 105 | | private static readonly Type Dt = typeof(DateTime?); |
| | 1 | 106 | | private static readonly Type Bl = typeof(bool?); |
| | | 107 | | |
| | | 108 | | private static Expression Build(Node node) |
| | | 109 | | { |
| | | 110 | | switch (node) |
| | | 111 | | { |
| | 8 | 112 | | case Num n: return Expression.Constant((decimal?)(decimal)n.Value, Dec); |
| | 9 | 113 | | case OrdersGridFormula.Str s: return Expression.Constant(s.Value, Str); |
| | 0 | 114 | | case Bool b: return Expression.Constant((bool?)b.Value, Bl); |
| | 0 | 115 | | case Null: return Expression.Constant(null, Dec); // тип уточнится при унификации |
| | | 116 | | case Field f: |
| | | 117 | | { |
| | 41 | 118 | | var key = ResolveFieldKey(f.Name) ?? f.Name; |
| | 41 | 119 | | if (!FieldExpressions.TryGetValue(key, out var factory)) |
| | 3 | 120 | | throw new NotTranslatableException($"Поле «{f.Name}» считается на сервере в памяти — сортировка и фи |
| | 38 | 121 | | return factory(); |
| | | 122 | | } |
| | | 123 | | case Un u: |
| | | 124 | | { |
| | 3 | 125 | | var a = Build(u.A); |
| | 3 | 126 | | return u.Op == "neg" ? Expression.Negate(AsDecimal(a, u.Pos)) : ToNullableBool(Expression.Not(AsBool(a)) |
| | | 127 | | } |
| | 17 | 128 | | case Bin b: return BuildBinary(b); |
| | 22 | 129 | | case Call c: return BuildCall(c); |
| | | 130 | | } |
| | | 131 | | |
| | 0 | 132 | | throw new NotTranslatableException("Неизвестный узел формулы."); |
| | | 133 | | } |
| | | 134 | | |
| | | 135 | | private static Expression BuildBinary(Bin b) |
| | | 136 | | { |
| | 17 | 137 | | var l = Build(b.L); |
| | 15 | 138 | | var r = Build(b.R); |
| | 15 | 139 | | switch (b.Op) |
| | | 140 | | { |
| | 1 | 141 | | case "and": return ToNullableBool(Expression.AndAlso(Truthy(l), Truthy(r))); |
| | 1 | 142 | | case "or": return ToNullableBool(Expression.OrElse(Truthy(l), Truthy(r))); |
| | 4 | 143 | | case "=": { Unify(ref l, ref r); return ToNullableBool(Expression.Equal(l, r)); } |
| | 0 | 144 | | case "<>": { Unify(ref l, ref r); return ToNullableBool(Expression.NotEqual(l, r)); } |
| | | 145 | | case "<": case ">": case "<=": case ">=": |
| | | 146 | | { |
| | 2 | 147 | | Unify(ref l, ref r); |
| | 2 | 148 | | if (l.Type == Str) |
| | | 149 | | { |
| | 0 | 150 | | var cmp = Expression.Call(typeof(string).GetMethod(nameof(string.Compare), [Str, Str])!, l, r); |
| | 0 | 151 | | l = cmp; r = Expression.Constant(0); |
| | | 152 | | } |
| | 4 | 153 | | var op = b.Op switch { "<" => ExpressionType.LessThan, ">" => ExpressionType.GreaterThan, "<=" => Expres |
| | 2 | 154 | | return ToNullableBool(Expression.MakeBinary(op, l, r)); |
| | | 155 | | } |
| | | 156 | | case "+": |
| | 1 | 157 | | if (l.Type == Str || r.Type == Str) return Concat(AsString(l), AsString(r)); |
| | 1 | 158 | | if (l.Type == Dt) return AddDays(l, AsDecimal(r, b.Pos)); |
| | 1 | 159 | | return Expression.Add(AsDecimal(l, b.Pos), AsDecimal(r, b.Pos)); |
| | | 160 | | case "-": |
| | 2 | 161 | | if (l.Type == Dt && r.Type == Dt) return DateDiffDays(r, l); |
| | 0 | 162 | | if (l.Type == Dt) return AddDays(l, Expression.Negate(AsDecimal(r, b.Pos))); |
| | 0 | 163 | | return Expression.Subtract(AsDecimal(l, b.Pos), AsDecimal(r, b.Pos)); |
| | 5 | 164 | | case "*": return Expression.Multiply(AsDecimal(l, b.Pos), AsDecimal(r, b.Pos)); |
| | | 165 | | case "/": |
| | | 166 | | { |
| | | 167 | | // Деление на ноль в MySQL даёт NULL — как на клиенте; для EF делаем явную проверку. |
| | 2 | 168 | | var rd = AsDecimal(r, b.Pos); |
| | 2 | 169 | | var zero = Expression.Constant((decimal?)0m, Dec); |
| | 2 | 170 | | return Expression.Condition(Expression.Equal(rd, zero), Expression.Constant(null, Dec), Expression.Divid |
| | | 171 | | } |
| | | 172 | | case "%": |
| | | 173 | | { |
| | 0 | 174 | | var rd = AsDecimal(r, b.Pos); |
| | 0 | 175 | | var zero = Expression.Constant((decimal?)0m, Dec); |
| | 0 | 176 | | return Expression.Condition(Expression.Equal(rd, zero), Expression.Constant(null, Dec), Expression.Modul |
| | | 177 | | } |
| | | 178 | | } |
| | | 179 | | |
| | 0 | 180 | | throw new NotTranslatableException($"Оператор «{b.Op}» не переводится."); |
| | | 181 | | } |
| | | 182 | | |
| | | 183 | | private static Expression BuildCall(Call c) |
| | | 184 | | { |
| | 22 | 185 | | var args = c.Args.Select(Build).ToList(); |
| | 21 | 186 | | switch (c.Name) |
| | | 187 | | { |
| | | 188 | | case "if": |
| | | 189 | | { |
| | 4 | 190 | | var a = args[1]; var b = args[2]; |
| | 2 | 191 | | Unify(ref a, ref b); |
| | 2 | 192 | | return Expression.Condition(Truthy(args[0]), a, b); |
| | | 193 | | } |
| | | 194 | | case "round": |
| | | 195 | | { |
| | 1 | 196 | | var digits = args.Count > 1 ? (int)ConstNumber(args[1], "округл: знаков") : 0; |
| | 1 | 197 | | var x = AsDecimal(args[0], c.Pos); |
| | 1 | 198 | | var round = typeof(Math).GetMethod(nameof(Math.Round), [typeof(decimal), typeof(int)])!; |
| | 2 | 199 | | return Lift(x, v => Expression.Call(round, v, Expression.Constant(digits))); |
| | | 200 | | } |
| | | 201 | | case "abs": |
| | | 202 | | { |
| | 0 | 203 | | var abs = typeof(Math).GetMethod(nameof(Math.Abs), [typeof(decimal)])!; |
| | 0 | 204 | | return Lift(AsDecimal(args[0], c.Pos), v => Expression.Call(abs, v)); |
| | | 205 | | } |
| | | 206 | | case "min": case "max": |
| | | 207 | | { |
| | | 208 | | // Попарно через CASE; NULL пропускается (COALESCE-подобно). |
| | 1 | 209 | | Expression acc = AsDecimal(args[0], c.Pos); |
| | 4 | 210 | | for (var i = 1; i < args.Count; i++) |
| | | 211 | | { |
| | 1 | 212 | | var next = AsDecimal(args[i], c.Pos); |
| | 1 | 213 | | var cmp = c.Name == "min" ? Expression.LessThan(acc, next) : Expression.GreaterThan(acc, next); |
| | 1 | 214 | | var pick = Expression.Condition(Expression.OrElse(Expression.Equal(next, Expression.Constant(null, D |
| | 1 | 215 | | acc = Expression.Condition(Expression.Equal(acc, Expression.Constant(null, Dec)), next, pick); |
| | | 216 | | } |
| | 1 | 217 | | return acc; |
| | | 218 | | } |
| | | 219 | | case "coalesce": |
| | | 220 | | { |
| | 1 | 221 | | Expression result = args[^1]; |
| | 4 | 222 | | for (var i = args.Count - 2; i >= 0; i--) |
| | | 223 | | { |
| | 2 | 224 | | var cur = args[i]; var rest = result; |
| | 1 | 225 | | Unify(ref cur, ref rest); |
| | 1 | 226 | | result = Expression.Coalesce(cur, rest); |
| | | 227 | | } |
| | 1 | 228 | | return result; |
| | | 229 | | } |
| | | 230 | | case "isempty": |
| | | 231 | | { |
| | 3 | 232 | | var a = args[0]; |
| | 6 | 233 | | if (a.Type == Str) return ToNullableBool(Expression.OrElse(Expression.Equal(a, Expression.Constant(null, |
| | 0 | 234 | | return ToNullableBool(Expression.Equal(a, Expression.Constant(null, a.Type))); |
| | | 235 | | } |
| | | 236 | | case "concat": |
| | | 237 | | { |
| | 3 | 238 | | Expression acc = AsString(args[0]); |
| | 24 | 239 | | for (var i = 1; i < args.Count; i++) acc = Concat(acc, AsString(args[i])); |
| | 3 | 240 | | return acc; |
| | | 241 | | } |
| | 0 | 242 | | case "upper": return StrCall(args[0], nameof(string.ToUpper)); |
| | 1 | 243 | | case "lower": return StrCall(args[0], nameof(string.ToLower)); |
| | 0 | 244 | | case "trim": return StrCall(args[0], nameof(string.Trim)); |
| | | 245 | | case "len": |
| | | 246 | | { |
| | 0 | 247 | | var s = AsString(args[0]); |
| | 0 | 248 | | return Expression.Condition(Expression.Equal(s, Expression.Constant(null, Str)), Expression.Constant(nul |
| | 0 | 249 | | Expression.Convert(Expression.Property(s, nameof(string.Length)), Dec)); |
| | | 250 | | } |
| | 1 | 251 | | case "contains": return StrPredicate(args, nameof(string.Contains)); |
| | 0 | 252 | | case "startswith": return StrPredicate(args, nameof(string.StartsWith)); |
| | 0 | 253 | | case "endswith": return StrPredicate(args, nameof(string.EndsWith)); |
| | | 254 | | case "text": |
| | 1 | 255 | | if (args[0].Type == Str) return args[0]; |
| | 1 | 256 | | throw new NotTranslatableException("текст(число/дата) не переводится в SQL — сортировка и фильтр недосту |
| | | 257 | | case "number": |
| | 0 | 258 | | if (args[0].Type == Dec) return args[0]; |
| | 0 | 259 | | throw new NotTranslatableException("число(текст) не переводится в SQL."); |
| | 1 | 260 | | case "today": return Expression.Constant((DateTime?)DateTime.Today, Dt); |
| | | 261 | | case "date": |
| | | 262 | | { |
| | 0 | 263 | | if (args[0] is ConstantExpression { Value: string text } && TryParseDate(text, out var d)) return Expres |
| | 0 | 264 | | if (args[0].Type == Dt) return args[0]; |
| | 0 | 265 | | throw new NotTranslatableException("дата(): в SQL переводится только дата-константа, например дата(\"31. |
| | | 266 | | } |
| | 1 | 267 | | case "days": return DateDiffDays(AsDate(args[1]), AsDate(args[0])); |
| | 2 | 268 | | case "adddays": return AddDays(AsDate(args[0]), AsDecimal(args[1], c.Pos)); |
| | 1 | 269 | | case "year": return DatePart(args[0], nameof(DateTime.Year)); |
| | 1 | 270 | | case "month": return DatePart(args[0], nameof(DateTime.Month)); |
| | 0 | 271 | | case "day": return DatePart(args[0], nameof(DateTime.Day)); |
| | | 272 | | case "weekday": |
| | | 273 | | { |
| | | 274 | | // DayOfWeek: 0 вс … 6 сб → 1 пн … 7 вс |
| | 1 | 275 | | var d = AsDate(args[0]); |
| | 1 | 276 | | var dow = Expression.Convert(Expression.Property(Expression.Property(d, "Value"), nameof(DateTime.DayOfW |
| | 1 | 277 | | var ru = Expression.Add(Expression.Modulo(Expression.Add(dow, Expression.Constant(6)), Expression.Consta |
| | 1 | 278 | | return Expression.Condition(Expression.Equal(d, Expression.Constant(null, Dt)), Expression.Constant(null |
| | | 279 | | } |
| | | 280 | | } |
| | | 281 | | |
| | 0 | 282 | | throw new NotTranslatableException($"Функция «{c.Name}» не переводится в SQL."); |
| | | 283 | | } |
| | | 284 | | |
| | | 285 | | // ---------- Помощники ---------- |
| | | 286 | | |
| | 32 | 287 | | private static Expression Prop(string name) => Expression.Property(Param, name); |
| | | 288 | | |
| | | 289 | | private static Expression NavProp(string nav, string prop) |
| | | 290 | | { |
| | 6 | 291 | | var n = Expression.Property(Param, nav); |
| | 6 | 292 | | var p = Expression.Property(n, prop); |
| | 6 | 293 | | var t = p.Type.IsValueType && Nullable.GetUnderlyingType(p.Type) == null ? typeof(Nullable<>).MakeGenericType(p. |
| | 6 | 294 | | return Expression.Condition(Expression.NotEqual(n, Expression.Constant(null, n.Type)), Expression.Convert(p, t), |
| | | 295 | | } |
| | | 296 | | |
| | 5 | 297 | | private static Expression NavName(string nav) => NavProp(nav, "Name"); |
| | | 298 | | |
| | | 299 | | private static Expression NavNavName(string nav1, string nav2) |
| | | 300 | | { |
| | 0 | 301 | | var n1 = Expression.Property(Param, nav1); |
| | 0 | 302 | | var n2 = Expression.Property(n1, nav2); |
| | 0 | 303 | | var name = Expression.Property(n2, "Name"); |
| | 0 | 304 | | var ok = Expression.AndAlso(Expression.NotEqual(n1, Expression.Constant(null, n1.Type)), Expression.NotEqual(n2, |
| | 0 | 305 | | return Expression.Condition(ok, name, Expression.Constant(null, Str)); |
| | | 306 | | } |
| | | 307 | | |
| | | 308 | | private static Expression ToDecimal(Expression e) |
| | | 309 | | { |
| | 4 | 310 | | if (e.Type == Dec) return e; |
| | 4 | 311 | | return Expression.Convert(e, Dec); |
| | | 312 | | } |
| | | 313 | | |
| | 14 | 314 | | private static Expression ToNullableBool(Expression e) => e.Type == Bl ? e : Expression.Convert(e, Bl); |
| | | 315 | | |
| | | 316 | | private static Expression Truthy(Expression e) |
| | | 317 | | { |
| | 9 | 318 | | if (e.Type == typeof(bool)) return e; |
| | 18 | 319 | | if (e.Type == Bl) return Expression.Equal(e, Expression.Constant((bool?)true, Bl)); |
| | 0 | 320 | | if (e.Type == Dec) return Expression.AndAlso(Expression.NotEqual(e, Expression.Constant(null, Dec)), Expression. |
| | 0 | 321 | | if (e.Type == Str) return Expression.AndAlso(Expression.NotEqual(e, Expression.Constant(null, Str)), Expression. |
| | 0 | 322 | | return Expression.NotEqual(e, Expression.Constant(null, e.Type)); |
| | | 323 | | } |
| | | 324 | | |
| | 3 | 325 | | private static Expression AsBool(Expression e) => Truthy(e); |
| | | 326 | | |
| | | 327 | | private static Expression AsDecimal(Expression e, int pos) |
| | | 328 | | { |
| | 42 | 329 | | if (e.Type == Dec) return e; |
| | 0 | 330 | | if (e.Type == Bl) return Expression.Condition(Expression.Equal(e, Expression.Constant((bool?)true, Bl)), Express |
| | 0 | 331 | | if (e.Type == Str || e.Type == Dt) throw new NotTranslatableException($"Ожидается число (позиция {pos + 1})."); |
| | 0 | 332 | | return Expression.Convert(e, Dec); |
| | | 333 | | } |
| | | 334 | | |
| | | 335 | | private static Expression AsDate(Expression e) |
| | | 336 | | { |
| | 14 | 337 | | if (e.Type == Dt) return e; |
| | 0 | 338 | | throw new NotTranslatableException("Ожидается дата: поле даты, сегодня() или дата(\"…\")."); |
| | | 339 | | } |
| | | 340 | | |
| | | 341 | | private static Expression AsString(Expression e) |
| | | 342 | | { |
| | 24 | 343 | | if (e.Type == Str) return e; |
| | 0 | 344 | | throw new NotTranslatableException("Склейка/текстовые функции в SQL — только для текстовых полей и строк (числа |
| | | 345 | | } |
| | | 346 | | |
| | | 347 | | /// <summary>Привести два операнда к общему типу (null-константа принимает тип соседа).</summary> |
| | | 348 | | private static void Unify(ref Expression a, ref Expression b) |
| | | 349 | | { |
| | 14 | 350 | | if (a.Type == b.Type) return; |
| | 0 | 351 | | if (a is ConstantExpression { Value: null }) { a = Expression.Constant(null, b.Type); return; } |
| | 0 | 352 | | if (b is ConstantExpression { Value: null }) { b = Expression.Constant(null, a.Type); return; } |
| | 0 | 353 | | if (a.Type == Bl && b.Type == Dec) { a = AsDecimal(a, 0); return; } |
| | 0 | 354 | | if (b.Type == Bl && a.Type == Dec) { b = AsDecimal(b, 0); return; } |
| | 0 | 355 | | throw new NotTranslatableException("Сравниваются значения разных типов."); |
| | | 356 | | } |
| | | 357 | | |
| | | 358 | | private static Expression Lift(Expression nullableDecimal, Func<Expression, Expression> body) |
| | | 359 | | { |
| | 1 | 360 | | var value = Expression.Property(nullableDecimal, "Value"); |
| | 1 | 361 | | return Expression.Condition(Expression.Equal(nullableDecimal, Expression.Constant(null, Dec)), Expression.Consta |
| | | 362 | | } |
| | | 363 | | |
| | | 364 | | /// <summary>Коллация для результата CONCAT: смесь колонок utf8mb4_bin и литералов даёт «Illegal mix of collations» |
| | | 365 | | private const string TextCollation = "utf8mb4_general_ci"; |
| | | 366 | | |
| | 1 | 367 | | private static readonly MethodInfo CollateMethod = typeof(RelationalDbFunctionsExtensions) |
| | 1 | 368 | | .GetMethod(nameof(RelationalDbFunctionsExtensions.Collate))!.MakeGenericMethod(Str); |
| | | 369 | | |
| | | 370 | | private static Expression Concat(Expression a, Expression b) |
| | | 371 | | { |
| | 6 | 372 | | var concat = Expression.Call(typeof(string).GetMethod(nameof(string.Concat), [Str, Str])!, a, b); |
| | 6 | 373 | | return Expression.Call(CollateMethod, Expression.Constant(EF.Functions), concat, Expression.Constant(TextCollati |
| | | 374 | | } |
| | | 375 | | |
| | | 376 | | private static Expression StrCall(Expression s, string method) |
| | | 377 | | { |
| | 1 | 378 | | var str = AsString(s); |
| | 1 | 379 | | var m = typeof(string).GetMethod(method, Type.EmptyTypes)!; |
| | 1 | 380 | | return Expression.Condition(Expression.Equal(str, Expression.Constant(null, Str)), Expression.Constant(null, Str |
| | | 381 | | } |
| | | 382 | | |
| | | 383 | | private static Expression StrPredicate(List<Expression> args, string method) |
| | | 384 | | { |
| | 1 | 385 | | var s = AsString(args[0]); |
| | 1 | 386 | | var sub = AsString(args[1]); |
| | 1 | 387 | | var m = typeof(string).GetMethod(method, [Str])!; |
| | 1 | 388 | | return ToNullableBool(Expression.AndAlso(Expression.NotEqual(s, Expression.Constant(null, Str)), Expression.Call |
| | | 389 | | } |
| | | 390 | | |
| | | 391 | | private static Expression DatePart(Expression d, string part) |
| | | 392 | | { |
| | 2 | 393 | | var date = AsDate(d); |
| | 2 | 394 | | var p = Expression.Convert(Expression.Property(Expression.Property(date, "Value"), part), Dec); |
| | 2 | 395 | | return Expression.Condition(Expression.Equal(date, Expression.Constant(null, Dt)), Expression.Constant(null, Dec |
| | | 396 | | } |
| | | 397 | | |
| | 1 | 398 | | private static readonly MethodInfo DateDiffDayMethod = typeof(MySqlDbFunctionsExtensions) |
| | 1 | 399 | | .GetMethod(nameof(MySqlDbFunctionsExtensions.DateDiffDay), [typeof(DbFunctions), typeof(DateTime), typeof(DateTi |
| | | 400 | | |
| | | 401 | | /// <summary>Дней от <paramref name="from"/> до <paramref name="to"/> (to − from), NULL если любая дата пустая.</sum |
| | | 402 | | private static Expression DateDiffDays(Expression from, Expression to) |
| | | 403 | | { |
| | 2 | 404 | | var call = Expression.Call(DateDiffDayMethod, Expression.Constant(EF.Functions), Expression.Property(from, "Valu |
| | 2 | 405 | | var anyNull = Expression.OrElse(Expression.Equal(from, Expression.Constant(null, Dt)), Expression.Equal(to, Expr |
| | 2 | 406 | | return Expression.Condition(anyNull, Expression.Constant(null, Dec), Expression.Convert(call, Dec)); |
| | | 407 | | } |
| | | 408 | | |
| | | 409 | | private static Expression AddDays(Expression date, Expression days) |
| | | 410 | | { |
| | 2 | 411 | | var add = typeof(DateTime).GetMethod(nameof(DateTime.AddDays), [typeof(double)])!; |
| | 2 | 412 | | var call = Expression.Call(Expression.Property(date, "Value"), add, Expression.Convert(Expression.Property(days, |
| | 2 | 413 | | var anyNull = Expression.OrElse(Expression.Equal(date, Expression.Constant(null, Dt)), Expression.Equal(days, Ex |
| | 2 | 414 | | return Expression.Condition(anyNull, Expression.Constant(null, Dt), Expression.Convert(call, Dt)); |
| | | 415 | | } |
| | | 416 | | |
| | | 417 | | private static double ConstNumber(Expression e, string what) |
| | | 418 | | { |
| | 2 | 419 | | if (e is ConstantExpression { Value: decimal d }) return (double)d; |
| | 0 | 420 | | throw new NotTranslatableException($"{what} — в SQL переводится только число-константа."); |
| | | 421 | | } |
| | | 422 | | |
| | | 423 | | private static bool TryParseDate(string text, out DateTime date) |
| | | 424 | | { |
| | 0 | 425 | | var formats = new[] { "dd.MM.yyyy", "yyyy-MM-dd", "d.M.yyyy" }; |
| | 0 | 426 | | return DateTime.TryParseExact(text.Trim(), formats, System.Globalization.CultureInfo.InvariantCulture, System.Gl |
| | | 427 | | } |
| | | 428 | | |
| | 23 | 429 | | private static string TypeOf(Type t) => t == Dec ? "number" : t == Str ? "text" : t == Dt ? "date" : t == Bl ? "bool |
| | | 430 | | } |