@query-graph/core 1.1.5 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 N-API for Rust
3
+ Copyright (c) 2026 Viktor Marichev
4
+ Copyright (c) 2020 N-API for Rust contributors
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - выбор JOIN только для реально запрошенных полей и условий;
18
18
  - scalar и list parameters без подстановки значений в SQL;
19
19
  - <code>exists</code>, <code>firstBy</code>, nullable projection objects и именованные ordering;
20
+ - вычисляемые проекции с переносимыми строковыми и числовыми функциями;
20
21
  - summary-графы с dimensions, measures, <code>GROUP BY</code> и <code>HAVING</code>;
21
22
  - batch relations для загрузки коллекций после пагинации root;
22
23
  - один семантический definition для SQL Server и Oracle.
@@ -38,8 +39,8 @@ npm install @query-graph/core
38
39
 
39
40
  ```ts
40
41
  import { registerDefinition } from '@query-graph/core'
41
- import { constraint, defineGraph, eq, param, project, requiredParameter, source } from '@query-graph/core/dsl'
42
- import type { QueryOperation, ResultOf } from '@query-graph/core/dsl'
42
+ import { constraint, defineGraph, eq, project, requiredParam, source } from '@query-graph/core/dsl'
43
+ import type { ResultOf } from '@query-graph/core/dsl'
43
44
 
44
45
  const users = source('users', {
45
46
  id: 'int64',
@@ -47,7 +48,7 @@ const users = source('users', {
47
48
  email: 'string',
48
49
  })
49
50
 
50
- const organisationId = requiredParameter('organisationId', 'int64')
51
+ const organisationId = requiredParam('organisationId', 'int64')
51
52
 
52
53
  const usersDefinition = defineGraph({
53
54
  name: 'users',
@@ -56,7 +57,7 @@ const usersDefinition = defineGraph({
56
57
  parameters: [organisationId],
57
58
  constraints: [
58
59
  constraint({
59
- predicate: eq(users.field('organisationId'), param(organisationId)),
60
+ predicate: eq(users.field('organisationId'), organisationId),
60
61
  }),
61
62
  ],
62
63
  projection: [
@@ -76,13 +77,8 @@ const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
76
77
  },
77
78
  })
78
79
 
79
- const operation = {
80
- select: ['id', 'email'],
81
- parameters: { organisationId: 42 },
82
- } as const satisfies QueryOperation<typeof usersDefinition>
83
-
84
- const statement = usersGraph.compileSqlServer(operation, { version: '2022' })
85
- type UserRow = ResultOf<typeof usersDefinition, typeof operation>
80
+ const statement = usersGraph.compileSqlServer({ parameters: { organisationId: 42 } }, { version: '2022' })
81
+ type UserRow = ResultOf<typeof usersDefinition>
86
82
  ```
87
83
 
88
84
  <code>statement</code> содержит:
@@ -95,16 +91,108 @@ type UserRow = ResultOf<typeof usersDefinition, typeof operation>
95
91
  | <code>objects</code> | presence metadata для nullable вложенных объектов |
96
92
  | <code>relations</code> | связи, выбранные planner для этой operation |
97
93
 
98
- Executor берет значения из <code>operation.parameters</code> по
94
+ Executor берет значения из <code>parameters</code> переданной operation по
99
95
  <code>bindings</code>, вызывает DB driver и восстанавливает объект по
100
96
  <code>columns</code> и <code>objects</code>. Разбирать SQL не нужно.
101
97
 
102
98
  Тот же mapped graph можно скомпилировать в Oracle:
103
99
 
104
100
  ```ts
105
- const oracleStatement = usersGraph.compileOracle(operation, { version: '19c' })
101
+ const oracleStatement = usersGraph.compileOracle({ parameters: { organisationId: 42 } }, { version: '19c' })
102
+ ```
103
+
104
+ ## Зачем нужен planner
105
+
106
+ В реальном графе преимущество проявляется, когда одна definition обслуживает
107
+ разные формы результата:
108
+
109
+ ```text
110
+ orders
111
+ |-- customer
112
+ | `-- organisation
113
+ |-- latestPayment [firstBy]
114
+ `-- items [batch many]
115
+
116
+ constraints: organisationId, exists(activePromotion)
117
+ ```
118
+
119
+ <code>ordersDefinition</code> содержит показанные на схеме relations и
120
+ projection. Его небанальные части выглядят так:
121
+
122
+ ```ts
123
+ const latestPayment = relation({
124
+ name: 'latestPayment',
125
+ from: orders,
126
+ to: payments,
127
+ on: eq(orders.field('id'), payments.field('orderId')),
128
+ selection: firstBy(desc(payments.field('createdAt'))),
129
+ })
130
+
131
+ const hasActivePromotion = constraint({
132
+ predicate: exists(promotions, eq(promotions.field('active'), true)),
133
+ })
134
+
135
+ const ordersDefinition = defineGraph({
136
+ name: 'orders',
137
+ root: orders,
138
+ sources: [orders, customers, organisations, payments, promotions],
139
+ parameters: [organisationId],
140
+ relations: [customer, customerOrganisation, latestPayment, activePromotion],
141
+ constraints: [byOrganisation, hasActivePromotion],
142
+ objects: [customerObject, latestPaymentObject],
143
+ projection: [orderId, customerName, customerOrganisationName, latestPaymentAmount],
144
+ })
145
+
146
+ const items = batchRelation({
147
+ name: 'items',
148
+ from: 'id',
149
+ query: batchQuery({
150
+ graph: orderItemsGraph,
151
+ key: { path: 'orderId', parameter: orderIds },
152
+ }),
153
+ cardinality: 'many',
154
+ })
155
+
156
+ const mappedOrders = registerDefinition(ordersDefinition).withRelationalMapping(orderMapping)
157
+ const ordersGraph = composeGraph({ root: mappedOrders, relations: [items] })
158
+ ```
159
+
160
+ Обычные source и projection declarations здесь свернуты в именованные
161
+ константы. <code>customerObject</code> и <code>latestPaymentObject</code> созданы
162
+ через <code>projectObject</code>, поэтому отсутствующая to-one строка становится
163
+ <code>null</code>.
164
+
165
+ Одна и та же definition теперь обслуживает дешевый список:
166
+
167
+ ```ts
168
+ const listPlan = ordersGraph.compileSqlServerPlan({
169
+ select: ['id'],
170
+ parameters: { organisationId: 42 },
171
+ })
172
+
173
+ listPlan.root.relations // []
174
+ listPlan.batches // []
175
+ ```
176
+
177
+ И подробное представление:
178
+
179
+ ```ts
180
+ const detailsPlan = ordersGraph.compileSqlServerPlan({
181
+ select: ['id', 'customer.name', 'latestPayment.amount', 'items.name'],
182
+ parameters: { organisationId: 42 },
183
+ })
184
+
185
+ detailsPlan.root.relations.map(({ name }) => name)
186
+ // ['customer', 'latestPayment']
187
+
188
+ detailsPlan.batches.map(({ name }) => name)
189
+ // ['items']
106
190
  ```
107
191
 
192
+ Planner не добавляет невыбранную <code>customerOrganisation</code>,
193
+ <code>exists(activePromotion)</code> остается semijoin без размножения root-строк,
194
+ а <code>items</code> загружается одним batch-запросом для всех найденных orders.
195
+
108
196
  ## Модель
109
197
 
110
198
  ```text
@@ -160,9 +248,9 @@ import { defineGraph, relation, project } from '@query-graph/core/dsl'
160
248
  ```
161
249
 
162
250
  <code>@query-graph/core/definition</code> сохраняет позиционные фабрики только
163
- для совместимости линии 1.2.x. <code>@query-graph/dsl-object</code> остается
164
- тонким deprecated facade над <code>@query-graph/core/dsl</code>. Новому коду эти
165
- entrypoint не нужны.
251
+ для обратной совместимости с ранним API 1.x.
252
+ <code>@query-graph/dsl-object</code> остается тонким deprecated facade над
253
+ <code>@query-graph/core/dsl</code>. Новому коду эти entrypoint не нужны.
166
254
 
167
255
  Batch-фабрики временно доступны из package root, но новый код должен импортировать
168
256
  их из <code>@query-graph/core/dsl</code>.
@@ -177,7 +265,3 @@ JavaScript.
177
265
  SQL execution намеренно остается у потребителя. Это позволяет использовать
178
266
  любой pool, transaction model, tracing и политику retry, не связывая planner с
179
267
  конкретным DB driver.
180
-
181
- ### Rule 34
182
-
183
- Every sufficiently generic abstraction eventually gets an implementation.
package/definition.d.ts CHANGED
@@ -30,7 +30,7 @@ import type {
30
30
  } from './dsl.js'
31
31
 
32
32
  /** @deprecated Import authoring primitives from `@query-graph/core/dsl`. */
33
- export const GRAPH_DEFINITION_VERSION: 10
33
+ export const GRAPH_DEFINITION_VERSION: 12
34
34
 
35
35
  /** @deprecated Use `source.field(name)` from `@query-graph/core/dsl`. */
36
36
  export function field<
package/definition.js CHANGED
@@ -65,13 +65,22 @@ exports.hidden = objectDsl.hidden
65
65
  exports.source = objectDsl.source
66
66
  exports.field = field
67
67
  exports.requiredParameter = objectDsl.requiredParameter
68
+ exports.requiredParam = objectDsl.requiredParam
68
69
  exports.optionalParameter = objectDsl.optionalParameter
70
+ exports.optionalParam = objectDsl.optionalParam
69
71
  exports.param = objectDsl.param
70
72
  exports.literal = objectDsl.literal
71
73
  exports.integer = objectDsl.integer
72
74
  exports.decimal = objectDsl.decimal
73
75
  exports.requiredListParameter = objectDsl.requiredListParameter
76
+ exports.requiredListParam = objectDsl.requiredListParam
74
77
  exports.optionalListParameter = objectDsl.optionalListParameter
78
+ exports.optionalListParam = objectDsl.optionalListParam
79
+ exports.requiredGroupsParameter = objectDsl.requiredGroupsParameter
80
+ exports.requiredGroupsParam = objectDsl.requiredGroupsParam
81
+ exports.optionalGroupsParameter = objectDsl.optionalGroupsParameter
82
+ exports.optionalGroupsParam = objectDsl.optionalGroupsParam
83
+ exports.allGroups = objectDsl.allGroups
75
84
  exports.eq = objectDsl.eq
76
85
  exports.neq = objectDsl.neq
77
86
  exports.lt = objectDsl.lt
@@ -86,11 +95,15 @@ exports.not = objectDsl.not
86
95
  exports.isNull = objectDsl.isNull
87
96
  exports.isNotNull = objectDsl.isNotNull
88
97
  exports.inParameter = objectDsl.inParameter
98
+ exports.inParam = objectDsl.inParam
89
99
  exports.exists = objectDsl.exists
90
100
  exports.lower = objectDsl.lower
91
101
  exports.upper = objectDsl.upper
92
102
  exports.coalesce = objectDsl.coalesce
93
103
  exports.concat = objectDsl.concat
104
+ exports.divide = objectDsl.divide
105
+ exports.truncate = objectDsl.truncate
106
+ exports.modulo = objectDsl.modulo
94
107
  exports.count = objectDsl.count
95
108
  exports.countDistinct = objectDsl.countDistinct
96
109
  exports.sum = objectDsl.sum
package/docs/authoring.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Authoring DSL
2
2
 
3
3
  Authoring API находится в <code>@query-graph/core/dsl</code>. Он создает
4
- семантический definition версии 10, который не содержит таблиц, SQL aliases или
4
+ семантический definition версии 12, который не содержит таблиц, SQL aliases или
5
5
  синтаксиса диалекта.
6
6
 
7
7
  ## Sources и поля
@@ -22,26 +22,124 @@ const users = source('users', {
22
22
  ## Параметры
23
23
 
24
24
  ```ts
25
- const id = requiredParameter('id', 'int64')
26
- const status = optionalParameter('status', 'string')
27
- const ids = requiredListParameter('ids', 'int64')
25
+ const id = requiredParam('id', 'int64')
26
+ const status = optionalParam('status', 'string')
27
+ const ids = requiredListParam('ids', 'int64')
28
+ const optionalIds = optionalListParam('optionalIds', 'int64')
28
29
  ```
29
30
 
30
- Scalar parameter используется через <code>param</code>, list parameter через
31
- <code>inParameter</code>. Optional parameter не активирует constraint с
31
+ Scalar parameter передается в expression напрямую, list parameter используется
32
+ через <code>inParam</code>. Явная форма <code>param(id)</code> поддерживается
33
+ для совместимости. Полные имена <code>requiredParameter</code>,
34
+ <code>optionalParameter</code>, <code>requiredListParameter</code>,
35
+ <code>optionalListParameter</code> и <code>inParameter</code> остаются алиасами
36
+ тех же factories. Optional parameter не активирует constraint с
32
37
  <code>when</code>, пока значение отсутствует.
33
38
 
39
+ ### Группированные параметры
40
+
41
+ Grouped list parameter описывает массив групп <code>{ key, values }</code>.
42
+ Каждая группа содержит один ключ и список значений:
43
+
44
+ ```ts
45
+ import { allGroups, and, constraint, eq, inParam, optionalGroupsParam, source } from '@query-graph/core/dsl'
46
+
47
+ const attributes = source('attributes', { name: 'string', valueId: 'int64' })
48
+ const filters = optionalGroupsParam('filters', { key: 'string', value: 'int64' })
49
+ const attributeFilter = constraint({
50
+ predicate: allGroups(filters, ({ key, values }) =>
51
+ and(eq(attributes.field('name'), key), inParam(attributes.field('valueId'), values)),
52
+ ),
53
+ })
54
+
55
+ // Operation parameters:
56
+ const parameters = {
57
+ filters: [
58
+ { key: 'color', values: [1, 2] },
59
+ { key: 'size', values: [3] },
60
+ ],
61
+ }
62
+ ```
63
+
64
+ Добавьте <code>filters</code> в <code>parameters</code> definition, а
65
+ <code>attributeFilter</code> в <code>constraints</code>. Если разные группы
66
+ должны совпасть с разными связанными строками, поместите <code>exists</code>
67
+ внутрь callback и объявите соответствующую relation.
68
+
69
+ <code>requiredGroupsParam</code> требует присутствия массива в operation.
70
+ Полные имена <code>optionalGroupsParameter</code> и
71
+ <code>requiredGroupsParameter</code> являются алиасами тех же factories.
72
+ Неизвестные поля конфигурации, scalar types и некорректные аргументы factories
73
+ отклоняются сразу при authoring.
74
+
75
+ Callback <code>allGroups</code> вызывается ровно один раз при построении AST,
76
+ а не для каждой runtime-группы. <code>key</code> является типизированным scalar
77
+ expression, <code>values</code> является отдельной scoped list reference только
78
+ для <code>inParam</code>/<code>inParameter</code>. Ни grouped parameter, ни
79
+ <code>values</code> нельзя передать в <code>param</code> или использовать как
80
+ обычное scalar expression. <code>values</code> также нельзя объявить root parameter.
81
+
82
+ Каждый binder получает UUID scope через <code>node:crypto.randomUUID()</code>,
83
+ независимо от загрузки отдельных экземпляров или копий DSL. Вложенный callback
84
+ может захватить <code>key</code> и <code>values</code> внешнего callback;
85
+ одноименные параметры не смешивают их scopes. Ссылки за пределами своего binder,
86
+ в том числе захваченные из соседнего callback, отклоняются Rust при регистрации.
87
+ Scope является внутренним идентификатором, его конкретное значение не гарантируется.
88
+
89
+ Семантика компиляции:
90
+
91
+ - отсутствующий optional parameter и пустой массив групп дают <code>true</code>;
92
+ - неактивное тело не требует optional scalar parameters или JOIN, используемых
93
+ только внутри него;
94
+ - predicates всех групп объединяются через <code>AND</code>;
95
+ - повторяющиеся ключи остаются независимыми группами, их values не объединяются;
96
+ - пустой <code>values</code> дает <code>false</code> в <code>inParam</code>;
97
+ - nullable predicate сохраняет обычную трехзначную SQL-семантику.
98
+
99
+ Rust разрешает <code>allGroups</code> только в constraint predicates, включая
100
+ вложенные boolean expressions и <code>exists</code>. Projection, ordering,
101
+ relation predicates и aggregate bodies не поддерживают binder.
102
+
103
+ Wire definition сохраняет тип элементов в <code>scalarType</code> и добавляет
104
+ <code>shape: 'groupedList'</code> и <code>keyType</code>. AST использует
105
+ <code>{ kind: 'allGroups', parameter, scope, predicate }</code>,
106
+ <code>{ kind: 'groupKey', scope }</code> и
107
+ <code>{ kind: 'inGroupValues', expression, scope }</code>.
108
+
34
109
  ## Expressions
35
110
 
36
111
  DSL предоставляет сравнения, boolean groups, null tests, semantic functions,
37
112
  агрегаты и literals:
38
113
 
39
114
  ```ts
40
- const predicate = and(eq(users.field('organisationId'), param(organisationId)), isNull(users.field('dateDelete')))
115
+ const predicate = and(eq(users.field('organisationId'), organisationId), isNull(users.field('dateDelete')))
116
+ ```
117
+
118
+ Semantic functions позволяют оставить вычисляемые поля частью графа, не
119
+ привязывая definition к синтаксису Oracle или SQL Server:
120
+
121
+ ```ts
122
+ const durationProjection = [
123
+ project({
124
+ path: 'hoursDuration',
125
+ expression: truncate(divide(services.field('durationMinutes'), 60)),
126
+ }),
127
+ project({
128
+ path: 'minutesDuration',
129
+ expression: modulo(services.field('durationMinutes'), 60),
130
+ }),
131
+ ]
41
132
  ```
42
133
 
43
- Типы operands проверяются TypeScript во время authoring и повторно Rust при
44
- регистрации.
134
+ <code>divide</code> возвращает decimal для точных числовых типов и float64,
135
+ если хотя бы один operand имеет тип float64. <code>truncate</code> отбрасывает
136
+ дробную часть по направлению к нулю и сохраняет scalar type. <code>modulo</code>
137
+ принимает int32, int64 и decimal; float64 отклоняется, чтобы definition одинаково
138
+ работал в поддерживаемых диалектах. Nullability распространяется от operands к
139
+ результату.
140
+
141
+ TypeScript выводит тип и nullability результата во время authoring. Rust при
142
+ регистрации повторно проверяет arity и совместимость типов operands.
45
143
 
46
144
  ## Relations
47
145
 
@@ -72,7 +170,7 @@ const profileRelation = relation({
72
170
  ```ts
73
171
  constraint({
74
172
  when: organisationId,
75
- predicate: eq(users.field('organisationId'), param(organisationId)),
173
+ predicate: eq(users.field('organisationId'), organisationId),
76
174
  })
77
175
 
78
176
  constraint({
@@ -19,7 +19,7 @@ Many-связь в одном SQL может размножить root-стро
19
19
  Child-модуль один раз закрепляет контракт ключа:
20
20
 
21
21
  ```ts
22
- const itemIds = requiredListParameter('itemIds', 'int64')
22
+ const itemIds = requiredListParam('itemIds', 'int64')
23
23
 
24
24
  const childrenQuery = batchQuery({
25
25
  graph: childrenGraph,
@@ -45,8 +45,32 @@ Rust выполняет semantic validation:
45
45
  Такое разделение дает ранние ошибки опечаток, не дублируя planner rules в
46
46
  JavaScript.
47
47
 
48
+ Циклические зависимости relation predicates через <code>exists</code> отклоняются
49
+ при регистрации definition: <code>invalidExistsContext</code> указывает на
50
+ <code>relations[i].on</code>, замыкающий цикл. Параметры, необходимые вложенным
51
+ <code>exists</code>, проверяются в фазе <code>operation</code>; отсутствующий
52
+ параметр получает <code>missingParameter</code> с location
53
+ <code>parameters.&lt;name&gt;</code>.
54
+
55
+ Статические параметры batch relation проверяются при <code>composeGraph</code>:
56
+ ошибки относятся к фазе <code>composition</code>, в том числе для имен параметров
57
+ с квадратными скобками. Параметр batch key исключается из этой проверки по точному
58
+ имени; значения ключей проверяются позже, при <code>compileBatch</code>, в фазе
59
+ <code>operation</code>.
60
+
48
61
  ## Совместимость diagnostics
49
62
 
50
- Wire schema остается версии 10. В совместимых выпусках сохраняются diagnostic
63
+ Wire schema версии 12 добавляет grouped-параметры и scoped-шаблоны `allGroups`.
64
+ Сохранённые raw definitions предыдущей версии требуют обновления; обычные
65
+ DSL-фабрики формируют актуальную версию автоматически. JS DSL и native binding
66
+ должны быть из одного выпуска пакета.
67
+
68
+ Невалидные данные группы получают `invalidParameterType` с точным путём,
69
+ например `parameters.characteristics[0].values[1]`. Ссылка вне своего шаблона
70
+ получает `unknownGroupScope`; повторное объявление активного scope получает
71
+ `shadowedGroupScope`. `allGroups` вне constraint predicate получает
72
+ `invalidAllGroupsContext`. Эти проверки выполняются и для raw wire definitions.
73
+
74
+ Для прежних ошибок сохраняются diagnostic
51
75
  codes и locations. Message предназначен человеку и не должен использоваться как
52
76
  machine-readable contract.
package/docs/sql.md CHANGED
@@ -33,6 +33,41 @@ const oracle = graph.compileOracle(operation, { version: '19c' })
33
33
  Version capabilities проверяются до возврата SQL. Например, pagination и
34
34
  <code>firstBy</code> могут требовать более новую версию диалекта.
35
35
 
36
+ Semantic functions рендерятся с одинаковым поведением через разные конструкции
37
+ диалектов:
38
+
39
+ | DSL | Oracle | SQL Server |
40
+ | -------------------------------- | ----------------------------- | --------------------------------- |
41
+ | <code>divide(left, right)</code> | <code>left / right</code> | дробное <code>left / right</code> |
42
+ | <code>truncate(value)</code> | <code>TRUNC(value)</code> | <code>ROUND(value, 0, 1)</code> |
43
+ | <code>modulo(left, right)</code> | <code>MOD(left, right)</code> | <code>left % right</code> |
44
+
45
+ SQL Server compiler принудительно переводит <code>divide</code> в дробную
46
+ арифметику, чтобы два integer operands не дали integer division.
47
+
48
+ В SQL Server 2012 и новее <code>concat</code> с одним аргументом компилируется
49
+ с дополнительной пустой строкой <code>''</code>. Списки длиннее 254 аргументов
50
+ разделяются на вложенные вызовы с сохранением порядка. Промежуточные группы
51
+ используют <code>nvarchar(max)</code>, чтобы избежать обрезания строк при
52
+ объединении. <code>NULL</code> обрабатывается как пустая строка согласно
53
+ [семантике CONCAT в SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/concat-transact-sql).
54
+
55
+ ## Boolean expressions
56
+
57
+ Компилятор различает значения и условия. Boolean-поле в <code>WHERE</code>,
58
+ <code>ON</code> или логическом выражении превращается в сравнение с <code>1</code>.
59
+ Условие в <code>SELECT</code>, <code>GROUP BY</code> или аргументе функции
60
+ превращается в <code>CASE</code>: <code>TRUE</code> дает <code>1</code>,
61
+ <code>FALSE</code> — <code>0</code>, <code>UNKNOWN</code> — <code>NULL</code>.
62
+ Boolean metadata и TypeScript-типы сохраняются; преобразование значений драйвера
63
+ остается ответственностью executor.
64
+
65
+ В Oracle 23ai условия сохраняют нативный SQL-тип <code>BOOLEAN</code> в контексте
66
+ значения. В контексте условия скалярные значения оборачиваются в
67
+ <code>TO_BOOLEAN</code>, который принимает как числовое, так и boolean-представление.
68
+ Прямые проекции полей и существующие сравнения с boolean literals сохраняют
69
+ прежний SQL.
70
+
36
71
  ## Bindings
37
72
 
38
73
  Compiler никогда не вставляет operation values в SQL. Каждый binding содержит:
@@ -40,11 +75,64 @@ Compiler никогда не вставляет operation values в SQL. Каж
40
75
  - SQL placeholder name;
41
76
  - имя parameter;
42
77
  - scalar type;
43
- - index элемента для list parameter.
78
+ - index элемента для list parameter;
79
+ - path до вложенного значения для grouped parameter.
44
80
 
45
81
  Повторное использование parameter переиспользует binding. Пустой list
46
82
  компилируется в ложное условие без placeholders.
47
83
 
84
+ Для scalar отсутствуют и `index`, и `path`. Для обычного list сохраняется
85
+ `index`. Для grouped list используется только `path`, например:
86
+
87
+ ```ts
88
+ ;[
89
+ { name: 'p0', parameter: 'characteristics', scalarType: 'int64', path: [0, 'key'] },
90
+ { name: 'p1', parameter: 'characteristics', scalarType: 'int64', path: [0, 'values', 1] },
91
+ ]
92
+ ```
93
+
94
+ Executor извлекает значение из `operation.parameters[binding.parameter]`,
95
+ последовательно проходя по сегментам `path`. `index` и `path` взаимно исключаются;
96
+ отсутствующий сегмент должен приводить к ошибке, а не к подстановке `null`.
97
+ Тип binding соответствует конкретному ключу или элементу values, а не контейнеру.
98
+ Повторное обращение к тому же параметру и пути переиспользует placeholder.
99
+ Потребитель должен обновить resolver bindings до начала использования grouped-параметров.
100
+
101
+ `allGroups` разворачивает шаблон в условия, соединённые `AND`, только при
102
+ компиляции операции. Для отсутствующего optional-параметра и пустого списка
103
+ групп результат равен `TRUE`; зависимости тела шаблона не включаются в план.
104
+ Каждый `exists` остаётся коррелированным подзапросом по описанному пути relations,
105
+ включая `firstBy`. Definition не изменяется, но SQL зависит от количества групп
106
+ и значений. Это один SQL-запрос, а не отдельное выполнение на каждую группу.
107
+
108
+ На один statement допускается до 10 000 разворачиваний тела `allGroups`
109
+ суммарно по всем шаблонам, включая вложенные. Превышение возвращает
110
+ `groupExpansionLimitExceeded`: вложенность может увеличить SQL многократно,
111
+ даже когда все bindings переиспользуются. Лимит применяется отдельно к каждой
112
+ компиляции и не ограничивает каталог доступных групп в приложении.
113
+
114
+ Также ограничено суммарное число элементов параметров-массивов, развёрнутых
115
+ внутри этих шаблонов: до 100 000 на statement. Повторное использование
116
+ одинаковых placeholders считается повторно, поскольку увеличивает SQL.
117
+ Ограничение охватывает и `values` группы, и обычный list parameter внутри
118
+ `allGroups`; превышение даёт `groupValueExpansionLimitExceeded`.
119
+
120
+ SQL Server compiler отклоняет statement с более чем 2100 уникальными bindings
121
+ с диагностикой `tooManyParameters`. Считаются все scalar, list и grouped bindings
122
+ вместе; разбиение `IN` не уменьшает их количество. Для больших выборок нужна
123
+ другая стратегия передачи данных, которой текущий grouped DSL не подменяет
124
+ явную ошибку. Ограничение согласовано с
125
+ [бюджетом параметров SQL Server](https://learn.microsoft.com/sql/sql-server/maximum-capacity-specifications-for-sql-server).
126
+
127
+ В Oracle большие <code>inParam</code> (включая values группы) и <code>inList</code> разбиваются на
128
+ группы <code>IN</code>, объединённые <code>OR</code> в общих скобках внутри
129
+ одного SQL. Размер группы — до 1000 выражений в 11g–21c и до 65535 в 23ai
130
+ согласно [лимитам Oracle](https://python-oracledb.readthedocs.io/en/v2.5.1/user_guide/bind.html#binding-a-large-number-of-items-in-an-in-list).
131
+ <code>not</code> применяется ко всей группе и сохраняет SQL-семантику
132
+ <code>NULL</code>. Порядок элементов и индексы bindings сохраняются, включая
133
+ дубликаты. Пустой литеральный <code>inList</code> по-прежнему отклоняется при
134
+ валидации definition.
135
+
48
136
  ## Result metadata
49
137
 
50
138
  SQL columns получают технические aliases <code>c0</code>,
@@ -16,6 +16,10 @@ TypeScript помогает автору до регистрации, Rust ос
16
16
  <code>int64</code> принимает number только в безопасном диапазоне JavaScript.
17
17
  Decimal string проверяется как конечная десятичная запись.
18
18
 
19
+ <code>integer(value)</code> поддерживает весь диапазон от
20
+ <code>Number.MIN_SAFE_INTEGER</code> до <code>Number.MAX_SAFE_INTEGER</code>,
21
+ включая значения за пределами 32 бит.
22
+
19
23
  ## QueryOperation
20
24
 
21
25
  Definition передает свои параметры, projection paths и ordering names в
@@ -34,6 +38,49 @@ const operation = {
34
38
  Required parameters обязательны, optional параметры могут отсутствовать,
35
39
  list parameters принимают readonly arrays.
36
40
 
41
+ Grouped list parameters выводятся как:
42
+
43
+ ```ts
44
+ readonly {
45
+ key: ScalarParameterValue<KeyType>
46
+ values: readonly ScalarParameterValue<ValueType>[]
47
+ }[]
48
+ ```
49
+
50
+ Например, <code>requiredGroupsParam('filters', { key: 'string', value: 'int64' })</code>
51
+ требует <code>parameters.filters</code> типа
52
+ <code>readonly { key: string; values: readonly (number | string)[] }[]</code>.
53
+ Optional grouped parameter может отсутствовать, но не заменяться на <code>null</code>.
54
+ Оба массива принимают readonly tuples, в том числе из <code>as const</code>.
55
+
56
+ Контейнер групп, отдельная группа и контейнер <code>values</code> не могут быть
57
+ <code>null</code>. Ключ и элементы списка следуют обычному scalar contract:
58
+ <code>null</code> запрещен для всех типов, кроме <code>json</code>. JSON сохраняет
59
+ существующую семантику JSON values, включая <code>null</code> и вложенные структуры.
60
+ Rust проверяет наличие <code>key</code>/<code>values</code>, неизвестные поля
61
+ группы и scalar types, включая ограничения числовых значений.
62
+
63
+ Grouped parameters сохраняют эти типы при композиции GraphModule и могут быть
64
+ статическими параметрами дочернего <code>batchRelation</code>. Ключ
65
+ <code>batchQuery</code> по-прежнему требует обычный list parameter, не grouped list.
66
+
67
+ В callback <code>allGroups</code> ключ имеет scalar type <code>KeyType</code>,
68
+ а <code>GroupValuesRef&lt;ValueType&gt;</code> принимается только как второй аргумент
69
+ <code>inParameter</code>/<code>inParam</code>. TypeScript исключает эту reference
70
+ из scalar expressions и root parameters; Rust проверяет lexical scope и
71
+ совместимость типов сравнения и membership.
72
+
73
+ Для grouped bindings <code>path</code> указывает на значение внутри operation
74
+ parameter: <code>[groupIndex, 'key']</code> или
75
+ <code>[groupIndex, 'values', valueIndex]</code>. Scalar bindings остаются без
76
+ <code>index</code>/<code>path</code>, обычные list bindings сохраняют
77
+ <code>index</code>. Поля <code>index</code> и <code>path</code> взаимоисключающие.
78
+ Это же правило действует для результатов <code>compileBatch</code>.
79
+
80
+ В Node API <code>offset</code> и <code>limit</code> принимают безопасные целые
81
+ JavaScript, включая значения больше <code>2 \*\* 32 - 1</code>.
82
+ <code>offset</code> должен быть неотрицательным, <code>limit</code> — положительным.
83
+
37
84
  ## Nullability
38
85
 
39
86
  Nullability выражения складывается из:
@@ -46,6 +93,20 @@ Nullability выражения складывается из:
46
93
  Required relation под optional ancestor остается nullable. Это свойство
47
94
  вычисляется из topology один раз при регистрации.
48
95
 
96
+ В <code>fieldType</code> флаг <code>nullable</code> типа <code>boolean</code>
97
+ означает, что поле может быть NULL. Литеральные <code>true</code> и
98
+ <code>false</code> позволяют вывести точную nullability.
99
+
100
+ Сравнения <code>eq</code>/<code>neq</code> с гарантированным <code>null</code>
101
+ (включая <code>literal(null)</code>) дают non-null результат. Тип операнда
102
+ <code>string | null</code> сам по себе не гарантирует сравнение именно с NULL:
103
+ nullable поле при сравнении со строкой может дать NULL.
104
+
105
+ <code>allGroups</code> возвращает boolean с nullability predicate шаблона.
106
+ Пустой набор групп дает <code>true</code>, но при наличии групп nullable
107
+ сравнение внутри callback может дать SQL UNKNOWN. Если callback возвращает
108
+ <code>exists</code>, результат остается non-null boolean.
109
+
49
110
  ## ResultOf
50
111
 
51
112
  ```ts
@@ -77,7 +138,8 @@ compilation не меняются.
77
138
  Rust проверяет:
78
139
 
79
140
  - ссылки на sources, fields и parameters;
80
- - scalar/list shape параметров;
141
+ - scalar/list/groupedList shape параметров и key type grouped parameters;
142
+ - lexical scopes ключей и списков групп, допустимость binder в constraint context;
81
143
  - совместимость сравнений и функций;
82
144
  - boolean тип predicates;
83
145
  - orderable и groupable expressions;