Забить нельзя доделать

В этом месяца, без фанатизма, пилил очередную подсистему провайдера для EFcore — локальные варианты серверных арифметических операций. Сложение, вычитание, умножение и деление. Третий и первый диалекты.

Вроде все получалось и неделю назад, даже, появилось ложное ощущение что «пронесёт» и все получится 🙂

Не пронесло.

Вчера добрался до последнего этапа этой стадии (или наоборот, не суть) — деление для первого диалекта.

И (снова) нарвался на мину фундаментальные ограничения базовой технологии, лежащей в основе всего этого поделия — LINQ.

  1. Компилятор может самостоятельно выполнить часть работы с константами (преобразовать тип, выполнить арифметическию операцию) и передать результат работы в LINQ.
  2. В LINQ не передается информация о явном и неявном конвертировании.

Но обо все попорядку. Попробую описать проблему как можно короче.

Берем простой пример.

int a=int.MaxValue;
int b=int.MaxValue;

На C# эти два числа корректно сложить не получится. Потому что результат сложения, по правилам языка, тоже int, а 4294967294 выходит за пределы допустимых значений int: [-2147483648..2147483647].

Сервер (Firebird, третий диалект) эти два числа сложит без проблем. Потому что он будет оперировать 64-битными числами. Запрос

select 1 from RDB$DATABASE where 2147483647+2147483647=4294967294

вернет одну запись.

На EFCore этот SQL запрос можно написать как-то так:

int a=int.MaxValue;
int b=int.MaxValue;

var recs=db.testTable.Where(r => (a+b==4294967294L);

Здесь компилятор C# начнет капать на мозги предупреждением CS0652 — «Comparison to integral constant is useless; the constant is outside the range of type ‘int’»

Поэтому лучше переписать так:

recs=db.testTable.Where(r => object.Equals(a+b,4294967294L));

Я, у себя в тестах, чтобы зацепить побольше кода, пишу так:

var recs=db.testTable.Where(r => (string)(object)(a+b)=="4294967294");

Поскольку в выражении участвуют локальные переменные, то EFCore применит оптимизацию — попробует самостоятельно сложить и сравнить. При этом, ясный пень, EFCore задействует стандартный алгоритм сложения двух int и посчитает неправильно.

Эту проблему можно обойти в провайдере для EFCore, предварительно преобразовав дерево выражения с подключением собственных алгоритмов (методов), в которых, в том числе, сложение двух Int32 дает Int64.

Не буду вдаваться в подробности, замечу что это достаточно кропотливый и нудный труд.

Здесь мы встречаем первую засаду — компилятор может самостоятельно вычислить часть выражений и передать результат в LINQ (EFCore).

Например, здесь компилятор откажется работать, выдав ошибку CS0220 «The operation overflows at compile time in checked mode«:

recs=db.testTable.Where(r => object.Equals(2147483647+2147483647,4294967294L));

Если написать:

recs=db.testTable.Where(r => object.Equals(unchecked(2147483647+2147483647),4294967294L));

он (компилятор) конечно пропустит этот код, но посчитает не так как нам хочется.

Еще одним примером, где «помощь» компилятора «дает под дых» — сложение строк.
На C# сложение с null обрабатывается как сложение с пустой строкой.
А на SQL — результат такого сложения даст NULL.
Но не будем отвлекаться, едем дальше.

Берем следующий пример.

int a=int.MaxValue;
int b=int.MaxValue;

long ab=4294967294L;

var recs=db.testTable.Where(r => (a+b==ab);

Компилятор приведет выражение «a+b==ab» к выражению «((long)(a+b)==ab». То есть, будет вызываться оператор сравнения для двух long.

Сложение:

Сравнение:

Создание ноды приведения левой части к long (вызов Expression.Convert):

Теперь давайте посмотрим на этот пример с точки зрения первого диалекта подключения к базе данных. Здесь сложение двух INTEGER дает результат DOUBLE. Правда правда. Запрос

select cast((cast(2147483647 as integer)+cast(2147483647 as integer)) as varchar(32)) from RDB$DATABASE

возвращает строку «4294967294.000000».

То есть, в текущем примере, компилятор нам сложение двух чисел будет принудительно приводить к Int64 (long), а у сервера в первом диалекте результатом сложения будет Double.

Заметьте, сами мы в выражении никаких приведений типов не делаем.

Если мы наш пример попробуем выполнить через EFCore на сервере с первым диалектом, то получится что-то вроде «(long)((double)a+(double)b)==ab».

Здесь пробразование к long — это нам компилятор C# подсунул. А приведение к double — ну это так работает сложение в первом диалекте.

По хорошему, в случае первого диалекта, нам нужно чтобы a+b возвращало double. Без всяких приведений. То есть приведение к long нужно выкинуть.

EFCore в подобных случаях, при генерации SQL, предлагает игнорировать преобразования short->int.

                if (innerType == convertedType
                    || (convertedType == typeof(int)
                        && (innerType == typeof(byte)
                            || innerType == typeof(sbyte)
                            || innerType == typeof(char)
                            || innerType == typeof(short)
                            || innerType == typeof(ushort))))
                {
                    return TryRemoveImplicitConvert(unaryExpression.Operand);
                }

Эту идею можно позаимствовать и расширить для игнорирования преобразования int->long.

И вот здесь возникает вторая проблема.

А если я наоборот ХОЧУ ЯВНО преобразовать результат сложения к long — «(long)(a+b)»? К примеру, чтобы потом этот результат привести к строке и получить «4294967294» вместо «4294967294.000000».

Да, в первом диалекте можно приводить к 64-битным целым, используя дыру в системе типов сервера. Сами её (дыру) найдете 🙂

Получается, что, в общем случае, неявные приведения надо выкидывать. А явные нужно сохранять.

Но компилятор сведения о том, какое преобразование применяется (явном или неявное) в LINQ не передает.

И это создает конкретные проблемы для реализации поддержки первого диалекта с его склонностью большую часть арифметических операций выполнять через … double.

С третьим диалектом вроде серьезных проблем (пока) нет. Он достаточно неплохо согласуется с правилами C#.


Так что, походу, про казино и дискотеки поддержку первого диалекта придется забыть.

С одной стороны жаль. А с другой — да и … с ним, с этим первым диалектом.

Будем считать, что его косяки несовместимы с косяками C#/LINQ да и самого EFCore, тоже.

One Comment

Вести с полей  on 11 февраля, 2022

[…] для сохранения поддержки первого диалекта, хотя было желание от неё избавиться… Без этого извращения […]

Leave a Comment