<!--
The template is not strict. You can use any ADR structure that feels best for a particular case.
See these ADR examples for inspiration:
- [Cassandra SEP - Ganeral Purpose Transactions](https://cwiki.apache.org/confluence/display/CASSANDRA/CEP-15%3A+General+Purpose+Transactions)
- [Rust RFC - Lifetime Ellision](https://github.com/rust-lang/rfcs/blob/master/text/0141-lifetime-elision.md)
- [TiKV - Use Joint Consensus](https://github.com/tikv/rfcs/blob/master/text/0054-joint-consensus.md)
-->
status: "accepted"
decision-makers: @UngiftedPoet, @funbringer

<!--
consulted: list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication
informed: list everyone who is kept up-to-date on progress; and with whom there is a one-way communication
-->

--------------------------------

# Summary

В данном документе описывается новая rule-based post-order система типов для SQL,
нацеленная на совместимость с PostgreSQL.

В начале рассматриваются проблемы старой системы.
В следующей секции определяются требования к новой системе.
Последующие главы описывают реализацию и интеграцию новой системы.
Последняя глава посвящена нерешенным проблемам и будущим работам.

# Мотивация

## Какие задачи решает система типов

В рамках системы типов решаются следующие задачи:

- вывод типов выражений;
- вывод типов параметров;
- добавление неявных приведений типов (коэрсий);
- проверка совместимости типов;

## Проблемы старой системы типов

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

## Примеры проблемных запросов

### Проверка совместимости типов

В проекции выражения типизируются и Sbroad обнаруживает ошибки:

```sql
picodata> select 1 + 'text';
---
- null
- 'sbroad: invalid expression: types unsigned and string are not supported for arithmetic
  expression (Expression(Row(Row { list: [NodeId { offset: 0, arena_type: Arena64
  }], distribution: None })) Add Expression(Row(Row { list: [NodeId { offset: 1, arena_type:
  Arena64 }], distribution: None })))'
...
```

Однако в случае фильтра анализ прекращается как только мы обнаруживаем оператор `<`,
который возвращает ожидаемый тип `bool`. Но из-за несовместимых типов в тарантуле происходит ошибка при попытке сравнения значений.

```sql
picodata> select 1 FROM (SELECT 1) WHERE 1 < 'kek';
---
- null
- 'sbroad: failed to create tarantool: Tarantool(BoxError { code: 171, message: Some("Type
  mismatch: can not convert string(''kek'') to number"), error_type: Some("ClientError"),
  errno: None, file: Some("./src/box/sql/mem.c"), line: Some(2784), fields: {}, cause:
  None })'
...
```

Сброд не валидирует совместимость типов колонок и источника данных в dml,
ошибка снова откладывается до момента исполнения:

```sql
-- create table t (a int, b int, primary key (a));
picodata> insert into t values (1, 'string');
2025-05-28 13:39:34.669 [173626] main/104/interactive vshard.rs:86 E> LuaError(ExecutionError("sbroad: failed to create transaction: RolledBack(Invalid(Value, Some(\"Failed to cast 'string' to integer.\")))")): dml_on_some
---
- null
- 'sbroad: Lua error (IR dispatch): LuaError(ExecutionError("sbroad: failed to create
  transaction: RolledBack(Invalid(Value, Some(\"Failed to cast ''string'' to integer.\")))"))'
...
```

Совместимость типов не проверяется, но тарантул умеет объединять значения разного типа,
поэтому запрос отрабатывает, хотя должна быть возвращена ошибка:

```sql
picodata> select * from (values('1234'), (1))
---
- metadata:
  - {'name': 'COLUMN_2', 'type': 'unsigned'}
  rows:
  - [1]
  - ['1234']
...
```
### Вывод типов выражений

В параметризированных запросах можно получить неизвестный тип:

```sql
picodata> pico.sql([[with t(a) as (select ?) select * from t]], {1})
---
- metadata:
  - {'name': 'a', 'type': 'unknown'}
  rows:
  - [1]
...
```

Общий тип при объединении вычисляется неверно:

```sql
picodata> select 1 union all select 1.0;
---
- metadata:
  - {'name': 'col_1', 'type': 'unsigned'}
  rows:
  - [1]
  - [1.0]
...
```

### Вывод типов параметров

Типы параметров определяются только после их подстановки (биндинга). Это плохо
работает с клиентами протокола PostgreSQL, которые полагаются на вывод типов
([детали](https://git.picodata.io/core/picodata/-/issues/648#note_82141)).
Для решения проблемы в [!1071 (merged)](https://git.picodata.io/core/picodata/-/merge_requests/1071)
была добавлена возможность указывать типы параметров явно через каст.
Попытка исполнить параметризованный запрос без кастов в pgproto приводит к ошибке:

```sql
postgres=> insert into t values ($1, $2) \bind 1 2 \g
ERROR:  picodata error: sbroad: could not determine data type of parameter $1
```

### Неявные приведения типов

Sbroad реализует оптимизации, которые фильтруют список бакетов исходя из условий `WHERE`.
Такая фильтрация позволяет отправлять распределенный запрос только на те узлы кластера,
которые содержат нужные данные. Для корректной фильтрации бакетов необходимы
неявные приведения (коэрсии) в операциях сравнения, которые ссылаются на ключи распределения.
В случае несовпадения типов, даже одинаковые значения могут вычислиться (захешироваться)
в разные `bucket_id`, в результате чего запрос будет исполнен неправильно
незаметно для пользователя:
- https://git.picodata.io/core/picodata/-/issues/1227,
- https://git.picodata.io/core/picodata/-/issues/1295,
- https://git.picodata.io/core/picodata/-/issues/1300.

```sql
picodata> explain select * from t where a = 1;
 ...
  - buckets = [1934]
...

picodata> explain select * from t where a = 1.0;
 ...
  - buckets = [712]
...
```

Строковые литералы не приводятся согласно контeксту. Следовательно, необходимы явные касты:

```sql
picodata> select localtimestamp = '2025-05-28T16:47:54.611018Z';
---
- null
- 'sbroad: failed to create tarantool: Tarantool(BoxError { code: 171, message: Some("Type
  mismatch: can not convert string(''2025-05-28T16:47:54.611018Z'') to datetime"),
  error_type: Some("ClientError"), errno: None, file: Some("./src/box/sql/mem.c"),
  line: Some(2784), fields: {}, cause: None })'
...
```

# Реализация новой системы типов

## Требования к новой реализации

Опыт работы с новой системой (UX) должен быть похожим на индустриальные аналоги.
В качестве эталона был взят PostgreSQL, к совместимости с которым мы стремимся.
В меньшей степени учитывался опыт CockroachDB и MySQL.

Свойства системы типов PostgreSQL определяют ключевые требования к новой системе:

 - **Rule-based:**
   Система основывается на правилах. Для каждого оператора или функции определяются
   перегрузка с типами аргументов и результата.

 - **Строгий type checking:**
   Все выражения строго типизируются и имеют конкретный тип,
   каждая перегрузка должна однозначно разрешаться, любые неоднозначности
   или несовместимости типов должны приводить к ошибке семантического анализа.

 - **Вывод типов параметров:**
   Типизация происходит с учетом типов параметров.
   Пользователь может явно указать типы; в противном случае они выводятся.
   Все типы параметров должны быть определены во время семантического анализа.

 - **Неявные приведения (коэрсии):**
   Типы выражений могут неявно приводиться, чтобы соответствовать ожидаемым типам в контексте.

Тем не менее, **полная совместимость с PostgreSQL не является целью.**
Это позволяет упростить систему типов и лучше учесть специфику реализации SQL
в Tarantool и VDBE-машины (к которым в конечном счете сводится исполнение запросов).

Можно выделить 2 основных ограничения:

 - **Все типы определяются за один проход по выражениям.**
   Многопроходные системы лучше распространяют информацию о типах параметров,
   однако они совершают больше работы. Также возникают дополнительные трудности
   при интерпретации результатов: порой пользователю может быть тяжело понять,
   каким образом выражения в разных частях запроса повлияли друг на друга
   с точки зрения вывода типов.

   Наша система будет однопроходной, анализ и распространение информации
   о типах будет происходить в одном направлении - _преимущественно_ слева направо.

 - **Неоднозначности вывода типов параметров не разрешаются системой.**
   Адекватную и интуитивно понятную стратегию разрешения неоднозначностей придумать
   довольно сложно, поэтому мы не ставим перед собой такую задачу. В большинстве
   практических случаев неоднозначностей не возникает.

   В случае возникновения неоднозначностей пользователю придется разрешать их самостоятельно,
   и это имеет свои положительные стороны. Например, снижается риск тихой поломки запросов
   при внесении изменений в алгоритм вывода типов.

### Краткое описание алгоритма

Новая система типов является строгой и основана на **правилах (перегрузках)**.
Каждое правило определяет типы аргументов и результата функции или оператора.
Для многих функций определены сразу несколько перегрузок; например, сложение
определено для типов `int`, `double`, `numeric`. Именно правила позволяют делать
выводы о типах тех или иных подвыражений.

Во время анализа дерева выражений верхние узлы распространяют на нижние **предпочтительные типы**,
с учетом которых происходит постепенное разрешение типов выражения.
Предпочтительный тип либо следует из ограничений схемы данных (например, типов колонок),
либо устанавливается по умолчанию равным `text`. Само разрешение типов начинается с
листовых выражений (детей) и постепенно распространяется на родительские выражения.

При анализе выражения алгоритм перебирает множество потенциально подходящих
перегрузок функций для того, чтобы выбрать самую удачную и с ее помощью вывести
типы аргументов и результата. Для каждой перегрузки ведется подсчет ее стоимости
исходя из примененных **неявных преобразований** и предпочтительного типа.

В конечном счете, для каждого выражения должна быть выбрана **единственная перегрузка**
с наименьшей стоимостью. Если перегрузку невозможно определить однозначно,
алгоритм завершается с ошибкой.

### Описание алгоритма

**Анализ реализуется рекурсивной функцией**, которая принимает выражение и предпочтительный тип.
Задача этой функции сводится к однозначному определению типа каждого подвыражения.
Результаты анализа кешируются и влияют на анализ всех последующих выражений,
которые будут пропущены через этот API.

Во время анализа **функция использует таблицы правил**, которые мы также называем
**перегрузками**. Правила задают количество и типы аргументов, а также
тип возвращаемого результата функции или оператора.

Например, для сложения можно определить следующие перегрузки:
- `+(int, int) -> int`
- `+(double, double) -> double`
- `+(numeric, numeric) -> numeric`

**Именно правила позволяют делать выводы о типах тех или иных подвыражений.**
При анализе выражения учитываются все возможные перегрузки функций, а для каждой
перегрузки анализ происходит отдельно с учетом типов аргументов.
При этом действуют несколько принципов:
- Типы аргументов могут не совпадать c ожидаемыми типами рассматриваемой перегрузки;
  в таком случае применяются **неявные приведения** (или коэрсии, от англ. coercion)
  для обеспечения строгого соответствия, **если приведение возможно**.
- Перегрузки сравниваются между собой в попытках выбрать **наиболее подходящую**.
  Для вычисления **стоимости** перегрузки ведется подсчет примененных неявных приведений.
- При сравнении всех потенциально подходящих перегрузок функций,
  возвращающие предпочтительный тип перегрузки имеют наибольший приоритет.

В конечном счете, для каждого выражения должна быть выбрана **единственная перегрузка**
с учетом предпочтительного типа, требующая **наименьшее число неявных приведений**
(иначе говоря, перегрузка с **наименьшей стоимостью**).
Если перегрузку невозможно определить однозначно, алгоритм завершается с ошибкой.

**Предпочтительный тип** (`desired_type` в коде) выражения **всегда известен**
и позволяет выбирать удачное значение по умолчанию или моделировать ограничения,
накладываемые схемой данных (например, при вставке данных в таблицу).
Этот тип определяется исходя из контекста, а именно:

- родительского выражения;
- реляционного оператора;
- типа `text`, который используется **по умолчанию**.

Возможны такие ситуации, когда финальный тип выражения будет отличаться от
предпочтительного; это не обязательно является ошибкой. Например, если для
выражения `1 + 1` был задан предпочтительный тип `text`, то выражение все
равно будет иметь целочисленный результат.

Тем не менее, бездумное применение алгоритма системы типов чревато логическими
ошибками при расхождении предпочтительного и фактического типов.
Например, в конструкции `WHERE 1.5` анализируемое выражение будет иметь тип
`numeric` **вопреки** предпочтительному типу `bool`, который следует из `WHERE`.
**С точки зрения ключевого алгоритма системы типов, ошибки нет**,
потому что он оперирует абстактными выражениями, а не конструкциями языка SQL.

В случаях, когда совместимость предпочтительного и фактического типов необходима,
использующий систему типов код должен самостоятельно произвести надлежащие проверки
после анализа. Мы ожидаем, что прикладной код учтет все дополнительные ограничения
предметной области.

Результатом анализа является отчет (`TypeReport` в коде), содержащий типы и
неявные приведения для каждого подвыражения. Отчет представляет собой хэш-таблицу,
отображающую уникальный идентификатор выражения в его тип.

Это позволяет:

 - **Перебирать разные варианты разрешения подвыражений.**
   В зависимости от выбранной перегрузки, одни и те же выражения могут иметь разные типы.
   Фиксация типов в выражении не позволит хранить разные варианты типов для оного выражения.

 - **Кэшировать результаты анализа для поддеревьев.**

 - **Отделить анализ типов от самих выражений.**
   Иными словами, сделать систему типов отчуждаемой от Sbroad
   и переиспользовать ее в других проектах.

#### Листовые узлы

Листовые узлы определяют базовые типы, от которых зависит типизация всего дерева.
На этих узлах завершается рекурсия, и, как и для всех узлов, результат их анализа
зависит от типа выражения и предпочтительного типа.

К листовым узлам относятся:

- **NULL:**
  Принимает предпочтительный тип, который всегда определен.

- **Литерал:**
  Определяeтся естественным типом и может быть неявно приведен к
  предпочтительному типу, если такое приведение существует.
  Например, `1` имеет естественный тип `int`, который может быть приведен
  к предпочтительным типам `double` или `numeric`.

- **Ссылкa:**
  Определяется типом согласно схеме.
  Может приводиться к предпочтительному типу.

- **Подзапрос:**
  Определяется вектором типов.
  Анализ подзапросов осуществляется отдельно.
  Может приводиться к предпочтительному типу.

- **Параметр:**
  Тип параметра может быть передан явно.
  Иначе он принимается равным предпочтительному типу.
  Может приводиться к предпочтительному типу.

#### Перегрузки функций

Разрешение перегрузок функций и операторов происходит согласно следующему алгоритму:

1) **Сначала определяются кандидаты.**

    a) С помощью имени анализируемой функции (или оператора) из таблиц правил
       выбираются только относящиеся к ней перегрузки.

    b) Отбрасываются перегрузки с числом аргументов, отличным от фактического.

    с) Для каждой выбранной перегрузки происходит анализ аргументов.
       Типы аргументов становятся предпочтительными типами при рекурсивном анализе.
       Если анализ всех аргументов завершился успешно,
       и все аргументы можно привести к типам аргументов перегрузки,
       то они приводятся, и перегрузка добавляется в множество возможных перегрузок.
       Ошибка анализа любого аргумента приводит к ошибке анализа всего выражения.

2) **Далеe определяется "оптимальная" перегрузка с учетом желаемого типа
   и количества необходимых приведений (стоимости).**

    a) Среди кандидатов, возвращающих желаемый тип, выбираются варианты с наименьшей стоимостью.
       Если такой кандидат **единственный**, то в качестве результата выбирается он.
       _В противном случае переходим на следующий шаг._

    b) Среди всех кандидатов выбираются варианты с наименьшей стоимостью.
       Если такой кандидат **единственный**, то в качестве результата выбирается он.
       _В противном случае переходим на следующий шаг._

    с) Если среди кандидатов с наименьшей стоимостью существует такой,
       что его возвращаемый тип может быть приведен к возвращаемым типам
       остальных кандидатов с наименьшей стоимостью, и этот кандидат
       **единственный**, то в качестве результата выбирается он.
       Таким образом, выбирается перегрузка с самым слабым типом,
       накладывающая наименьшие ограничения на приведения при дальнейшем анализе.
       _В противном случае переходим на следующий шаг._

    d) Анализ завершается с ошибкой.

#### Анализ однородных выражений (COALESCE, CASE, etc)

Под однородными выражениями в данном случае понимаются выражения, которые
можно привести к общему типу. В частности, однородными выражениями являются
ветви оператора `CASE`, аргументы функции `COALESCE`, аргументы операторов
сравнения (`IN`, `=`, `<` ...), элементы `VALUES` и проч.

Анализ однородных выражений сводится к определению общего типа
и происходит по следующему алгоритму:

1) **Определяются возможные типы (кандидаты) согласно анализируемым выражениям.**

    a) Каждое выражение анализируется дважды: с указанным предпочтительным типом
       и с предпочтительным типом по умолчанию (`text`). В результате анализа
       это выражение получает тип. Каждый полученный тип становится кандидатом
       для выведения общего типа.

2) **Среди кандидатов выбирается общий тип.**

    a) Если _предпочтительный тип_ оказался среди кандидатов, то
       все выражения анализируются с этим предпочтительным типом. Тогда,
       если все выражения можно привести к предпочтительному типу, то
       к выражениям применяются неявные приведения, и алгоритм
       останавливается на этом кандидате.
       _В противном случае переходим на следующий шаг._

    b) Для каждого кандидата осуществляется анализ всех выражений
       с ним в качестве предпочтительного типа. Тогда, если все выражения
       можно привести к этому **и только этому** кандидату, то
       к выражениям применяются неявные приведения, и алгоритм
       останавливается на этом кандидате.
       _В противном случае переходим на следующий шаг._

    c) Из множества кандидатов отбираются только те, к которым можно
       привести каждое выражение. Среди отобранных кандидатов должен найтись
       **единственный** _минимальный тип_, то есть, такой тип, который можно
       привести к остальным из них. Если такой тип существует, то к выражениям
       применяются неявные приведения, и алгоритм останавливается на нём.
       _В противном случае переходим на следующий шаг._

    d) Анализ завершается с ошибкой.

#### Приведения литералов

Для некоторых литералов существуют уникальные приведения:

- Литералы с плавающей точкой могут иметь тип `numeric` или `double`.
  `numeric` является типом по умолчанию, но в контексте с типом `double` литерал
  приобретает тип `double`.

- Строковые литералы могут приводиться к любому типу.
  `text` является типом по умолчанию, но в контексте с другим типом литерал к нему приводится.

Приведения литералов происходят согласно предпочтительному типу.
Если существует приведение для литерала в предпочтительный тип, то оно происходит.

#### Вывод типов параметров

Типы параметров могут быть определены пользователем.
В таком случае они фиксируются и не меняются при анализе.

Если тип параметра не указан, система выводит его согласно контексту использования.

Во время анализа выражения, параметр с неизвестным типом приобретает предпочтительный тип.
Предпочтительным типом по умолчанию является `text`, поэтому он же является
типом по умолчанию для параметра, что соответствует PostgreSQL.

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

Выражение не всегда однозначно задает тип параметра.
Один и тот же параметр может встретиться выражении несколько раз с разным предпочтительным типом.
Например, в выражении `$1::int = $1::numeric` тип параметра задан неоднозначно
(`int` или `numeric`) с помощью каста.
Система не пытается разрешать такие неоднозначности, поэтому происходит ошибка.

#### Разбор примеров

##### `SELECT 1 + $1`

Предпочтительный тип имеет значение по умолчанию - `text`.

При анализе оператора `+` рассматриваем следующие перегрузки:

1) `+(int, int) -> int`

    анализируем аргументы:

    1) `1::int`, `desired_type = int` => `1::int` (`1` имеет тип `int`)

    2) `$1::unknown`, `desired_type = int` => `$1::int` (`$1` имеет тип `int`)

    стоимость равна 0, так как нет приведений.

2) `+(double, double) -> double`

    анализируем аргументы:

    1) `1::int`, `desired_type = double` => `1::int::double` (`1` имеет тип `int` и приводится к `double`)

    2) `$1::unknown`, `desired_type = double` => `$1::double`

    стоимость равна 1, так как литерал приводится.

3) `+(numeric, numeric) -> numeric`

    анализируем аргументы:

    1) `1::int`, `desired_type = numeric` => `1::int::numeric`

    2) `$1::unknown`, `desired_type = numeric` => `$1::numeric`

    стоимость равна 1, так как литерал приводится.

Нет перегрузок, возвращающих предпочтительный тип, поэтому анализируем все перегрузки.
Наименьшая стоимость у перегрузки `+(int, int) -> int` и она единственная,
поэтому останавливаемся на ней.

##### `SELECT 1 + 1.5`

Предпочтительный тип имеет значение по умолчанию - `text`.

При анализе оператора `+` рассматриваем следующие перегрузки:

1) `+(int, int) -> int`

    анализируем аргументы:

    1) `1::int`, `desired_type = int` => `1::int`

    2) `1.5::numeric`, `desired_type = int` => `1.5::numeric`

    `1.5` не может быть приведен к `int`, перегрузка не рассматривается.

2) `+(double, double) -> double`

    анализируем аргументы:

    1) `1::int`, `desired_type = double` => `1::int::double`

    2) `1.5::numeric`, `desired_type = double` => `1.5::numeric::double` (специальное приведение для литерала)

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

3) `+(numeric, numeric) -> numeric`

    анализируем аргументы:

    1) `1::int`, `desired_type = numeric` => `1::int::numeric`

    2) `1.5::numeric`, `desired_type = numeric` => `1.5::numeric`

    стоимость равна 1, так как литерал `1` приводится.

Нет перегрузок, возвращающих предпочтительный тип, поэтому анализируем все перегрузки.
Наименьшая стоимость у перегрузки `+(numeric, numeric) -> numeric` и она единственная,
поэтому останавливаемся на ней.

##### `SELECT CASE WHEN true THEN $1 ELSE $2 END + 1`

Предпочтительный тип имеет значение по умолчанию - `text`.

При анализе оператора `+` рассматриваем следующие перегрузки:

1) `+(int, int) -> int`

    анализируем аргументы:

    1) `1::int`, `desired_type = int` => `1::int`

    2) `CASE WHEN true THEN $1 ELSE $2 END`, `desired_type = int` =>
       кандидаты на общий тип: `int` и `text`, `desired_type = int` =>
       `CASE WHEN true THEN $1::int ELSE $2::int END::int`

    стоимость равна 0, так как нет приведений.

2) `+(double, double) -> double`

    анализируем аргументы:

    1) `1::int`, `desired_type = double` => `1::int::double`

    2) `CASE WHEN true THEN $1 ELSE $2 END`, `desired_type = double` =>
       кандидаты на общий тип: `double` и `text`, `desired_type = double` =>
       `CASE WHEN true THEN $1::double ELSE $2::double END::double`

    стоимость равна 1, так как литерал `1` приводится.

3) `+(numeric, numeric) -> numeric`

    анализируем аргументы:

    1) `1::int`, `desired_type = numeric` => `1::int::numeric`

    2) `CASE WHEN true THEN $1 ELSE $2 END`, `desired_type = numeric` =>
       кандидаты на общий тип: `numeric` и `text`, `desired_type = numeric` =>
       `CASE WHEN true THEN $1::numeric ELSE $2::numeric END::numeric`

    стоимость равна 1, так как литерал `1` приводится.

Нет перегрузок, возвращающих предпочтительный тип, поэтому анализируем все перегрузки.
Наименьшая стоимость у перегрузки `+(int, int) -> int` и она единственная,
поэтому останавливаемся на ней.

##### `INSERT INTO t (double_col) VALUES (1.5)`

Типизация `INSERT` начинается с анализа `VALUES`.

Предпочтительный тип определяется согласно типу колонки: `desired_type = double`.

Литерал `1.5` по умолчанию имеет тип `numeric`, но при `desired_type = double` он приводится к `double`.
`VALUES` возвращает строку с единствсенным значением типа `double`,
который совемстим с модифицируемсой колонкой, типизация завершается успешно.

##### `SELECT $1 + 1, $1 + 1.5`

Анализ выражений происходит слева направо.
Сначала анализируем `$1 + 1`, выражение и параметр приобретают тип `int`.
Далее анализируем `$1 + 1.5`, тип параметра уже зафиксирован,
анализируем сложение параметре типа `int` и литерала с плавающей точкой.
Выражение будет иметь тип `numeric`, типом параметра останется `int`.

#### Сравнение с PostgreSQL

Для простых выражений можно ожидать высокой степени совместимости с PostgreSQL.
Примеры таких выражений:

| Raw                       | Typed                                                      |
|---------------------------|------------------------------------------------------------|
| `SELECT 1 = $1`           | `SELECT 1::int = $1::int`                                  |
| `SELECT 1.5 + $1`         | `SELECT 1.5::numeric + $1::numeric`                        |
| `SELECT $1`               | `SELECT $1::text`                                          |
| `SELECT $1 + 1, $1 + 1.5` | `SELECT $1::int + 1::int, $1::int::numeric + 1.5::numeric` |

Однако, есть и различия:

- `SELECT $1 FROM t WHERE $1 = 1`

  PostgreSQL выводит типы строго слева направо, поэтому тип параметра
  будет определен в проекции и примет значение по умолчанию `text`.
  После чего анализ фильтра приведет к ошибке сравнения `text` и `int`.

  В Sbroad сперва будет типизирован фильтр, параметр приобретет тип `int`.
  В проекции будет использован уже выведенный тип. Запрос считается корректным.

- `SELECT 1 + $1 + 1.5` и `SELECT 1.5 + $1 + 1` 

  В новой системе вывод типов не зависит от ассоциативности операторов,
  потому что предпочтительный тип будет определен либо соседним выражением,
  либо выражением выше. Поэтому тип параметра всегда будет `numeric`.

  PostgreSQL осуществляет более локальный анализ,
  поэтому тип параметра всегда будет определяться соседним операндом.
  В первом запросе параметр будет иметь тип `int`, а во втором - `numeric`

### Интеграция новой системы типов

Система реализована в виде отдельного модуля с собственным представлением для выражений.
Для взаимодействия с системой необходима конвертация во внутреннее представление системы анализируемого выражения.
Такой подход имеет свои накладные расходы, но он позволяет разрабатывать систему независимо.
Также это упростит переход на новый парсер, так как зависимость от представления Sbroad минимальна,
и позволит переиспользовать систему в других проектах.

Предполагаемый сценарий использования системы типов выглядит следующим образом:

1) Для запроса строится AST, в котором известны типы колонок и литералов.
2) Выражения из AST конвертируются во внутреннее представление системы типов и типизируется.
3) С учетом выведенных типов строится строго типизированный план.
   При построении плана добавляются касты согласно неявным преобразованиям.

На момент 962e5b13ecbaa5ec23d6fe0abab58567ba744d15 в Sbroad такой подход невозможен,
так как в нем отсутствует строгое разделение на парсинг и построение плана.
Поэтому система работает с уже построенным представлением и выполняет только
проверки типов, неявные приведения выражений и вывод типов параметров.
Вывод типов литераловосуществляет старая система Sbroad.

### Нерешенные проблемы, будущие работы

На момент 962e5b13ecbaa5ec23d6fe0abab58567ba744d15 основной проблемой является
неполная интеграция новой системы в Sbroad. Новая система выполняет проверки типов,
неявные приведения выражений и вывод типов параметров.
Вывод типов осуществляет старая система Sbroad.
В итоге останется только новая система, но это потребует
[существенных изменений в парсере](https://git.picodata.io/core/picodata/-/work_items/1647).

Без изменений в парсере можно решить некоторые проблемы, требующие приведения:

- [Приведения в условиях фильтрации `WHERE` для корректного подсчета распределений](https://git.picodata.io/core/picodata/-/issues/1777)
- [Приведения для объединяемых колонок в `UNION`/`EXCEPT`](https://git.picodata.io/core/picodata/-/work_items/1645)
- [Приведения присваиваемых значений в DML](https://git.picodata.io/core/picodata/-/issues/1812)

Также многие ошибки из Sbroad могут быть улучшены после добавления их в систему типов:

- https://git.picodata.io/core/picodata/-/work_items/1648
- https://git.picodata.io/core/picodata/-/work_items/1649

Для отладки вывода типов следует добавить в SQL новую функцию
[typeof](https://git.picodata.io/core/picodata/-/issues/1919) по аналогии
с PostgreSQL и sqlite:

```sql
SELECT typeof($1);
```
