@query-graph/core 1.1.4 → 1.1.6

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/README.md CHANGED
@@ -3,47 +3,44 @@
3
3
  Типизированное описание графов запросов, нативное планирование и компиляция SQL
4
4
  для Node.js.
5
5
 
6
- `query-graph` позволяет один раз описать доступные данные и связи без SQL, а для
7
- каждой операции выбрать поля, параметры, сортировку и пагинацию. Rust проверяет
8
- definition, строит минимальный план связей и компилирует его в SQL Server или
9
- Oracle SQL.
6
+ Граф описывает доступные данные, связи, ограничения и форму результата без
7
+ синтаксиса конкретной СУБД. Rust валидирует definition, выбирает минимальный
8
+ набор связей и компилирует operation в SQL Server или Oracle SQL.
10
9
 
11
- Пакет намеренно **не подключается к базе данных и не выполняет SQL**. Он
12
- возвращает SQL, bindings и metadata результата; способ исполнения остаётся у
13
- приложения.
10
+ Пакет не подключается к базе данных и не выполняет запросы. На выходе он
11
+ возвращает SQL, bindings и metadata, необходимые executor приложения.
14
12
 
15
- Основные возможности:
13
+ ## Возможности
16
14
 
17
- - строгий TypeScript DSL для sources, parameters, relations и projections;
18
- - runtime-валидация definition и operation в Rust;
19
- - выбор только необходимых JOIN по запрошенным projection paths;
20
- - один definition для SQL Server и Oracle;
21
- - scalar/list parameters без подстановки значений в SQL;
22
- - `exists`, детерминированные to-one relations, summary-графы и batch relations;
23
- - тип результата через `ResultOf`.
15
+ - объектный TypeScript DSL с выводом параметров, projection paths и результата;
16
+ - строгая runtime-валидация definition, mapping и operation в Rust;
17
+ - выбор JOIN только для реально запрошенных полей и условий;
18
+ - scalar и list parameters без подстановки значений в SQL;
19
+ - <code>exists</code>, <code>firstBy</code>, nullable projection objects и именованные ordering;
20
+ - вычисляемые проекции с переносимыми строковыми и числовыми функциями;
21
+ - summary-графы с dimensions, measures, <code>GROUP BY</code> и <code>HAVING</code>;
22
+ - batch relations для загрузки коллекций после пагинации root;
23
+ - один семантический definition для SQL Server и Oracle.
24
24
 
25
25
  Требуется Node.js 20 или новее.
26
26
 
27
- ## Быстрый старт
28
-
29
- ### Установка
27
+ ## Установка
30
28
 
31
29
  ```bash
32
30
  npm install @query-graph/core
33
31
  ```
34
32
 
35
- Основной объектный DSL уже входит в пакет и доступен через
36
- `@query-graph/core/dsl`.
33
+ Используйте два entrypoint:
37
34
 
38
- ### 1. Опишите доступные данные
35
+ - <code>@query-graph/core/dsl</code> для описания графа и TypeScript типов;
36
+ - <code>@query-graph/core</code> для регистрации и нативной компиляции.
39
37
 
40
- Definition использует логические имена. Здесь нет имён таблиц, SQL-алиасов или
41
- синтаксиса конкретной СУБД.
38
+ ## Быстрый старт
42
39
 
43
40
  ```ts
44
41
  import { registerDefinition } from '@query-graph/core'
45
- import { constraint, defineGraph, eq, param, project, requiredParameter, source } from '@query-graph/core/dsl'
46
- 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'
47
44
 
48
45
  const users = source('users', {
49
46
  id: 'int64',
@@ -51,7 +48,7 @@ const users = source('users', {
51
48
  email: 'string',
52
49
  })
53
50
 
54
- const organisationId = requiredParameter('organisationId', 'int64')
51
+ const organisationId = requiredParam('organisationId', 'int64')
55
52
 
56
53
  const usersDefinition = defineGraph({
57
54
  name: 'users',
@@ -60,7 +57,7 @@ const usersDefinition = defineGraph({
60
57
  parameters: [organisationId],
61
58
  constraints: [
62
59
  constraint({
63
- predicate: eq(users.field('organisationId'), param(organisationId)),
60
+ predicate: eq(users.field('organisationId'), organisationId),
64
61
  }),
65
62
  ],
66
63
  projection: [
@@ -68,14 +65,7 @@ const usersDefinition = defineGraph({
68
65
  project({ path: 'email', expression: users.field('email'), default: true }),
69
66
  ],
70
67
  })
71
- ```
72
-
73
- ### 2. Добавьте реляционное отображение
74
68
 
75
- Mapping связывает логический source с физической таблицей. Его можно заменить,
76
- не меняя definition.
77
-
78
- ```ts
79
69
  const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
80
70
  sources: {
81
71
  users: {
@@ -86,972 +76,192 @@ const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
86
76
  },
87
77
  },
88
78
  })
89
- ```
90
79
 
91
- Mapping переиспользуемых частей можно держать рядом с ними и объединять при
92
- регистрации итогового графа:
93
-
94
- ```ts
95
- const usersGraph = registerDefinition(usersDefinition).withRelationalMappings([usersMapping, profileMapping])
80
+ const statement = usersGraph.compileSqlServer({ parameters: { organisationId: 42 } }, { version: '2022' })
81
+ type UserRow = ResultOf<typeof usersDefinition>
96
82
  ```
97
83
 
98
- Фрагменты могут дополнять `columns` одного source, если указывают одинаковую
99
- таблицу. Повтор одинакового отображения безопасен. Разные таблицы для одного
100
- source или разные физические колонки для одного логического поля возвращают
101
- структурированную ошибку фазы `mapping`. После объединения mapping всё равно
102
- должен полностью покрывать sources итогового definition.
84
+ <code>statement</code> содержит:
103
85
 
104
- `registerDefinition` валидирует и индексирует definition в Rust. Обычно граф
105
- регистрируется один раз при запуске приложения и затем переиспользуется.
86
+ | Поле | Назначение |
87
+ | ---------------------- | --------------------------------------------------------------------------------- |
88
+ | <code>sql</code> | параметризованный SQL с placeholders <code>p0</code>, <code>p1</code> и так далее |
89
+ | <code>bindings</code> | связь placeholder с operation parameter и типом |
90
+ | <code>columns</code> | связь SQL alias <code>cN</code> с логическим projection path |
91
+ | <code>objects</code> | presence metadata для nullable вложенных объектов |
92
+ | <code>relations</code> | связи, выбранные planner для этой operation |
106
93
 
107
- ### 3. Скомпилируйте операцию
94
+ Executor берет значения из <code>parameters</code> переданной operation по
95
+ <code>bindings</code>, вызывает DB driver и восстанавливает объект по
96
+ <code>columns</code> и <code>objects</code>. Разбирать SQL не нужно.
108
97
 
109
- Operation содержит только изменяемую часть запроса. TypeScript проверяет имена
110
- полей и параметров относительно конкретного definition.
98
+ Тот же mapped graph можно скомпилировать в Oracle:
111
99
 
112
100
  ```ts
113
- const operation = {
114
- select: ['id', 'email'],
115
- parameters: {
116
- organisationId: 42,
117
- },
118
- } as const satisfies QueryOperation<typeof usersDefinition>
119
-
120
- const statement = usersGraph.compileSqlServer(operation)
121
-
122
- console.log(statement.sql)
123
- console.table(statement.bindings)
124
- console.table(statement.columns)
125
-
126
- type UserRow = ResultOf<typeof usersDefinition, typeof operation>
127
- // { id: number | string; email: string }
101
+ const oracleStatement = usersGraph.compileOracle({ parameters: { organisationId: 42 } }, { version: '19c' })
128
102
  ```
129
103
 
130
- Для SQL Server получится параметризованный запрос:
104
+ ## Зачем нужен planner
131
105
 
132
- ```sql
133
- SELECT
134
- [t0].[id] AS [c0],
135
- [t0].[email] AS [c1]
136
- FROM [dbo].[users] AS [t0]
137
- WHERE
138
- ([t0].[organisation_id] = @p0)
139
- ```
140
-
141
- В `statement.bindings` placeholder `p0` будет связан с параметром
142
- `organisationId` типа `int64`.
143
-
144
- Тот же graph можно передать другому compiler:
145
-
146
- ```ts
147
- const oracleStatement = usersGraph.compileOracle(operation, { version: '19c' })
148
- ```
149
-
150
- ### 4. Выполните SQL своим драйвером
151
-
152
- Результат компиляции не содержит соединение с базой данных:
153
-
154
- | Поле statement | Назначение |
155
- | -------------- | ------------------------------------------------------------------------------ |
156
- | `sql` | SQL с именованными placeholders `p0`, `p1` и так далее |
157
- | `bindings` | связь placeholder с параметром operation, его типом и индексом элемента списка |
158
- | `columns` | связь физической колонки `cN` с projection path и scalar type |
159
- | `objects` | presence-маркеры для nullable вложенных объектов |
160
- | `relations` | связи, фактически выбранные planner для этой операции |
161
-
162
- Executor приложения берёт значения из `operation.parameters` согласно
163
- `statement.bindings`, передаёт их DB driver и восстанавливает логические поля по
164
- `statement.columns`. Разбирать SQL для этого не требуется.
165
-
166
- ## Как выбрать возможность
167
-
168
- | Задача | API или раздел |
169
- | -------------------------------------------------------- | ----------------------------------------------------------- |
170
- | Выбрать связанные поля `0..1` или `1` | [`relation` и `firstBy`](#to-one-selection) |
171
- | Отфильтровать root по наличию связанных строк | [`exists`](#existential-constraints) |
172
- | Переиспользовать часть definition | [GraphModule](#композиция-через-graphmodule) |
173
- | Передать массив в `IN (...)` | [Параметры-списки](#параметры-списки) |
174
- | Выбирать сортировку во время выполнения | [Именованные сортировки](#именованные-сортировки) |
175
- | Получить агрегированный результат | [Summary-графы](#summary-графы) |
176
- | Отличить отсутствующий объект от объекта с `NULL`-полями | [Presence объектной проекции](#presence-объектной-проекции) |
177
- | Загрузить коллекцию после пагинации root | [Batch-связи](#batch-связи-и-двухфазное-выполнение) |
178
- | Обработать структурированную ошибку | [Диагностика](#диагностика) |
179
-
180
- ## Модель использования
181
-
182
- | Понятие | Когда создаётся | За что отвечает |
183
- | ---------------------- | ----------------------------------- | ------------------------------------------------------- |
184
- | `GraphDefinition` | при объявлении графа | логические sources, relations, constraints и projection |
185
- | `QueryGraph` | один раз после `registerDefinition` | проверенный и проиндексированный нативный граф |
186
- | `RelationalMapping` | при конфигурации приложения | физические таблицы и переименования колонок |
187
- | `QueryOperation` | для конкретного запроса | выбранные поля, параметры, ordering и pagination |
188
- | `CompiledSqlStatement` | при компиляции operation | SQL и metadata, необходимые executor приложения |
189
- | `GraphModule` | при композиции definition | переиспользуемый фрагмент без собственного root |
190
-
191
- Обычный жизненный цикл выглядит так:
106
+ В реальном графе преимущество проявляется, когда одна definition обслуживает
107
+ разные формы результата:
192
108
 
193
109
  ```text
194
- defineGraph -> registerDefinition -> withRelationalMapping(s)
195
- |
196
- QueryOperation -----------------------+-> compileSqlServer / compileOracle
197
- |
198
- +-> executor приложения -> DB driver
199
- ```
200
-
201
- ## Архитектура пакета
202
-
203
- Cargo workspace разделён на два слоя. `query-graph-core` не зависит от Node.js
204
- или N-API и содержит definition, type system, planner, relational mapping и SQL
205
- compilers. Корневой crate `query_graph` является тонким N-API adapter: он
206
- десериализует wire-объекты, вызывает core и переводит результат и diagnostics в
207
- JavaScript.
208
-
209
- Зависимость направлена только от adapter к core. Исполнение SQL в оба слоя не входит.
210
-
211
- ## Определение графа
212
-
213
- Определение графа не зависит от диалекта SQL и содержит:
214
-
215
- - логические источники и поля;
216
- - типизированные параметры;
217
- - именованные связи;
218
- - семантические ограничения;
219
- - выходную проекцию;
220
- - именованные варианты сортировки.
110
+ orders
111
+ |-- customer
112
+ | `-- organisation
113
+ |-- latestPayment [firstBy]
114
+ `-- items [batch many]
221
115
 
222
- `registerDefinition` один раз передает определение в Rust. Rust валидирует все
223
- ссылки, создает переиспользуемые индексы и возвращает нативный дескриптор
224
- `QueryGraph`.
225
-
226
- ```ts
227
- import { registerDefinition } from '@query-graph/core'
228
- import {
229
- asc,
230
- constraint,
231
- desc,
232
- defineGraph,
233
- defineGraphModule,
234
- eq,
235
- exists,
236
- firstBy,
237
- inParameter,
238
- isNotNull,
239
- nullable,
240
- ordering,
241
- param,
242
- project,
243
- relation,
244
- requiredListParameter,
245
- requiredParameter,
246
- source,
247
- } from '@query-graph/core/dsl'
248
-
249
- const users = source('users', {
250
- id: 'int64',
251
- email: 'string',
252
- })
253
-
254
- const profiles = source('profiles', {
255
- id: 'int64',
256
- userId: 'int64',
257
- displayName: nullable('string'),
258
- })
259
-
260
- const userId = requiredParameter('userId', 'int64')
261
- const profileRelation = relation({
262
- name: 'profile',
263
- from: users,
264
- to: profiles,
265
- on: eq(users.field('id'), profiles.field('userId')),
266
- cardinality: 'one',
267
- required: true,
268
- selection: firstBy(asc(profiles.field('id'))),
269
- })
270
-
271
- const userProfileModule = defineGraphModule({
272
- name: 'userProfile',
273
- sources: [users, profiles],
274
- parameters: [userId],
275
- relations: [profileRelation],
276
- constraints: [
277
- constraint({
278
- predicate: eq(users.field('id'), param(userId)),
279
- }),
280
- constraint({
281
- predicate: exists(profiles, isNotNull(profiles.field('displayName'))),
282
- }),
283
- ],
284
- projection: [
285
- project({
286
- path: 'id',
287
- expression: users.field('id'),
288
- default: true,
289
- }),
290
- project({
291
- path: 'profile.displayName',
292
- expression: profiles.field('displayName'),
293
- }),
294
- ],
295
- orderings: [
296
- ordering({
297
- name: 'idAsc',
298
- by: [asc(users.field('id'))],
299
- default: true,
300
- }),
301
- ],
302
- })
303
-
304
- const definition = defineGraph({
305
- name: 'userProfile',
306
- root: users,
307
- modules: [userProfileModule],
308
- })
309
-
310
- const graph = registerDefinition(definition)
116
+ constraints: organisationId, exists(activePromotion)
311
117
  ```
312
118
 
313
- ### Композиция через GraphModule
314
-
315
- `GraphModule` группирует переиспользуемую часть определения: источники,
316
- параметры, связи, ограничения, проекцию и сортировку. У модуля нет собственного
317
- корневого источника, и его нельзя зарегистрировать или скомпилировать отдельно.
318
-
319
- `defineGraph` объединяет вложенные модули и локальные элементы в один плоский
320
- `GraphDefinition`. Повторно подключенный объект дедуплицируется по identity. Для
321
- именованных элементов разные определения с одним ключом приводят к ошибке
322
- композиции.
323
-
324
- Constraints не имеют имени: повторно использованный constraint добавляется один
325
- раз, а разные constraint-объекты сохраняются и объединяются через `AND`. Сведения
326
- о модулях не передаются в Rust и не входят в wire-формат.
327
-
328
- Вспомогательные функции API описания графа формируют версионируемое и
329
- сериализуемое промежуточное представление `GraphDefinition`. Дискриминаторы
330
- выражений, например `kind: 'eq'`, существуют только в сгенерированном
331
- представлении. В прикладном коде графа их писать не требуется. TypeScript
332
- проверяет ссылки на поля источников.
333
-
334
- Relation path проекции указывать не требуется. При регистрации definition Rust
335
- находит самый глубокий source, использованный expression, и один раз сохраняет
336
- путь от root до этого source. Expression может обращаться к root и любым sources
337
- на одной ветке. Обращение к двум независимым веткам отклоняется как неоднозначная
338
- проекция. Planner использует уже вычисленные пути и не повторяет inference при
339
- каждой компиляции operation.
340
-
341
- ### Варианты DSL
342
-
343
- Canonical `GraphDefinition` не зависит от authoring DSL. Основной authoring API
344
- доступен через `@query-graph/core/dsl` и использует объектные конфигурации. Позиционный
345
- `@query-graph/core/definition` сохранён как адаптер совместимости для существующего кода;
346
- новые графы на него ориентировать не следует.
347
-
348
- Отдельный пакет `@query-graph/dsl-object` является тонким фасадом над тем же API.
349
- Он полезен, когда DSL нужен как отдельная зависимость:
350
-
351
- ```bash
352
- npm install @query-graph/core @query-graph/dsl-object
353
- ```
119
+ <code>ordersDefinition</code> содержит показанные на схеме relations и
120
+ projection. Его небанальные части выглядят так:
354
121
 
355
122
  ```ts
356
- import { registerDefinition } from '@query-graph/core'
357
- import { asc, defineGraph, eq, firstBy, project, relation, source } from '@query-graph/dsl-object'
358
-
359
- const users = source('users', { id: 'int64' })
360
- const profiles = source('profiles', {
361
- id: 'int64',
362
- userId: 'int64',
363
- displayName: 'string',
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'))),
364
129
  })
365
130
 
366
- const definition = defineGraph({
367
- name: 'users',
368
- root: users,
369
- sources: [users, profiles],
370
- relations: [
371
- relation({
372
- name: 'profile',
373
- from: users,
374
- to: profiles,
375
- on: eq(users.field('id'), profiles.field('userId')),
376
- cardinality: 'one',
377
- selection: firstBy(asc(profiles.field('id'))),
378
- }),
379
- ],
380
- projection: [
381
- project({ path: 'id', expression: users.field('id'), default: true }),
382
- project({ path: 'profile.displayName', expression: profiles.field('displayName') }),
383
- ],
384
- })
385
-
386
- const graph = registerDefinition(definition)
387
- ```
388
-
389
- Фасад не содержит второй реализации и напрямую переэкспортирует
390
- `@query-graph/core/dsl`. Расширение позиционного compatibility API не меняет поверхность
391
- объектного DSL.
392
-
393
- Оба входа формируют один и тот же versioned wire-контракт. Сторонний DSL также может
394
- создавать `GraphDefinitionInput`; Rust проверяет его при `registerDefinition`.
395
-
396
- ### Existential constraints
397
-
398
- `exists(source, predicate?)` задаёт семантический квантор. В graph constraint
399
- Rust по умолчанию использует уникальный relation path от root до указанного source,
400
- поэтому описывать SQL-подзапрос или correlation columns не требуется.
401
-
402
- ```ts
403
- constraint({
404
- predicate: exists(memberships, eq(memberships.field('teamId'), param(teamId))),
405
- })
406
- ```
407
-
408
- Predicate может обращаться к root и sources на выведенном пути. Ссылки на
409
- соседнюю ветку отклоняются при регистрации definition. Помимо constraints,
410
- явно коррелированный `exists` разрешён в `relation.on`; в projections, ordering
411
- и selection он отклоняется. Отрицание выражается обычным `not(exists(...))`.
412
-
413
- Planner воспринимает existential branch как semijoin. Sources этой ветки не
414
- добавляются во внешние JOIN и `statement.relations`, поэтому relation с
415
- cardinality `many` не размножает корневые строки и не мешает их пагинации.
416
- Oracle и SQL Server compiler строят коррелированный `EXISTS` из одной и той же
417
- семантики definition.
418
-
419
- По умолчанию `exists` коррелируется от root. Для semijoin относительно уже
420
- выбранного внешнего source можно явно указать точку корреляции:
421
-
422
- ```ts
423
- constraint({
424
- predicate: exists(permissions, permissionPredicate, { from: memberships }),
425
- })
426
- ```
427
-
428
- `from` должен быть строгим предком целевого source на relation path. Planner
429
- оставляет путь до `from` во внешнем запросе, а оставшийся путь компилирует как
430
- коррелированный semijoin.
431
-
432
- В `relation.on` точка корреляции обязательна. Это позволяет сначала ограничить
433
- множество кандидатов связи, а затем применить `firstBy`:
434
-
435
- ```ts
436
- relation({
437
- name: 'profile',
438
- from: users,
439
- to: profiles,
440
- on: and(
441
- eq(users.field('id'), profiles.field('userId')),
442
- exists(profileFlags, eq(profileFlags.field('enabled'), true), { from: profiles }),
443
- ),
444
- selection: firstBy(asc(profiles.field('id'))),
445
- })
446
- ```
447
-
448
- Такой `exists` становится semijoin внутри relation predicate. Его branch не
449
- материализуется во внешние JOIN и не меняет cardinality результата.
450
-
451
- ### Параметры-списки
452
-
453
- Параметр имеет явную форму `scalar` или `list`. Один параметр не меняет форму
454
- между операциями, поэтому в прикладном коде не требуется поддерживать
455
- `number | number[]`.
456
-
457
- ```ts
458
- const users = source('users', {
459
- id: 'int64',
460
- })
461
-
462
- const memberships = source('memberships', {
463
- id: 'int64',
464
- userId: 'int64',
465
- teamId: 'int64',
466
- })
467
-
468
- const teamIds = requiredListParameter('teamIds', 'int64')
469
-
470
- const definition = defineGraph({
471
- name: 'usersByTeams',
472
- root: users,
473
- sources: [users, memberships],
474
- parameters: [teamIds],
475
- relations: [
476
- relation({
477
- name: 'memberships',
478
- from: users,
479
- to: memberships,
480
- on: eq(users.field('id'), memberships.field('userId')),
481
- cardinality: 'many',
482
- }),
483
- ],
484
- constraints: [
485
- constraint({
486
- predicate: exists(memberships, inParameter(memberships.field('teamId'), teamIds)),
487
- }),
488
- ],
489
- projection: [project({ path: 'id', expression: users.field('id'), default: true })],
490
- })
491
- ```
492
-
493
- Operation передает обычный массив:
494
-
495
- ```ts
496
- const statement = relationalGraph.compileSqlServer({
497
- parameters: { teamIds: [12, 18, 24] },
498
- })
499
- ```
500
-
501
- Compiler разворачивает его в `IN (@p0, @p1, @p2)`. Каждый элемент получает
502
- binding `{ parameter: 'teamIds', index, scalarType: 'int64' }`, по которому
503
- adapter исполнения берет значение из исходного массива. Rust проверяет каждый
504
- элемент отдельно и возвращает путь ошибки наподобие
505
- `parameters.teamIds[2]`.
506
-
507
- Отсутствующий optional list отключает constraint с `when`. Переданный пустой
508
- список остается присутствующим параметром и компилируется в ложный предикат
509
- `1 = 0`; недопустимый SQL `IN ()` не создается.
510
-
511
- ### Именованные сортировки
512
-
513
- Definition объявляет допустимые способы упорядочивания, а operation выбирает один из них по имени:
514
-
515
- ```ts
516
- const definition = defineGraph({
517
- // ...
518
- orderings: [
519
- ordering({ name: 'createdDesc', by: [desc(items.field('dateCreate'))], default: true }),
520
- ordering({ name: 'nameAsc', by: [asc(items.field('name'))] }),
521
- ],
131
+ const hasActivePromotion = constraint({
132
+ predicate: exists(promotions, eq(promotions.field('active'), true)),
522
133
  })
523
134
 
524
- relationalGraph.compileOracle({
525
- ordering: 'nameAsc',
526
- parameters,
527
- })
528
- ```
529
-
530
- Поле `default: true` можно указать только у одного варианта. Если operation не передает
531
- `ordering`, planner использует default-вариант; без него SQL строится без `ORDER BY`.
532
- TypeScript выводит union имен из definition и подключенных graph modules. Rust проверяет имя
533
- повторно и планирует только связи и параметры выбранной сортировки.
534
-
535
- ### To-one selection
536
-
537
- `cardinality: 'one'` описывает контракт связи. Если физический источник может
538
- содержать несколько кандидатов, `firstBy` задает семантическое правило выбора
539
- одной строки:
540
-
541
- ```ts
542
- const profile = relation({
543
- name: 'profile',
544
- from: users,
545
- to: profiles,
546
- on: eq(users.field('id'), profiles.field('userId')),
547
- selection: firstBy(asc(profiles.field('id'))),
548
- })
549
- ```
550
-
551
- Поля сортировки могут обращаться только к target source. Список сортировки
552
- обязателен; последнее поле следует выбирать уникальным, чтобы результат был
553
- детерминированным. `required: false` означает `0..1`, а `required: true` -
554
- ровно одну связанную строку.
555
-
556
- SQL Server renderer использует `OUTER/CROSS APPLY` и `TOP (1)`, Oracle 12c+
557
- использует `OUTER/CROSS APPLY` и `FETCH FIRST 1 ROW ONLY`. Эти детали не входят
558
- в definition. Для Oracle 11g `firstBy` возвращает
559
- `unsupportedDialectFeature`.
560
-
561
- ### Summary-графы
562
-
563
- `defineSummaryGraph` описывает результат через dimensions и measures, не через
564
- SQL-конструкции `GROUP BY` и `HAVING`:
565
-
566
- ```ts
567
- const teams = source('teams', {
568
- id: 'int64',
569
- workspaceId: 'int64',
570
- })
571
-
572
- const memberships = source('memberships', {
573
- id: 'int64',
574
- userId: 'int64',
575
- teamId: 'int64',
576
- })
577
-
578
- const workspaceId = requiredParameter('workspaceId', 'int64')
579
- const minimumMembers = requiredParameter('minimumMembers', 'int64')
580
-
581
- const teamId = dimension({
582
- path: 'teamId',
583
- expression: teams.field('id'),
584
- default: true,
585
- })
586
-
587
- const memberCount = measure({
588
- path: 'memberCount',
589
- expression: countDistinct(memberships.field('userId')),
590
- default: true,
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],
591
144
  })
592
145
 
593
- const definition = defineSummaryGraph({
594
- name: 'teamSummary',
595
- root: teams,
596
- sources: [teams, memberships],
597
- parameters: [workspaceId, minimumMembers],
598
- relations: [
599
- relation({
600
- name: 'members',
601
- from: teams,
602
- to: memberships,
603
- on: eq(teams.field('id'), memberships.field('teamId')),
604
- cardinality: 'many',
605
- }),
606
- ],
607
- constraints: [
608
- constraint({
609
- predicate: eq(teams.field('workspaceId'), param(workspaceId)),
610
- }),
611
- constraint({
612
- predicate: gte(memberCount, param(minimumMembers)),
613
- }),
614
- ],
615
- dimensions: [teamId],
616
- measures: [memberCount],
617
- orderings: [
618
- ordering({
619
- name: 'memberCountDesc',
620
- by: [desc(memberCount)],
621
- default: true,
622
- }),
623
- ],
624
- })
625
- ```
626
-
627
- Planner включает все объявленные dimensions в идентичность результата и
628
- компилирует их в `GROUP BY`, даже если operation не возвращает часть из них.
629
- Constraint без агрегата фильтрует исходное множество через `WHERE`. Constraint
630
- с агрегатом фильтрует уже построенные группы через `HAVING`. Поэтому автор графа
631
- не выбирает SQL-фазу вручную и не может случайно поместить агрегат в `WHERE`.
632
-
633
- Поддерживаются `count()`, `count(expression)`, `countDistinct`, `sum`,
634
- `average`, `minimum` и `maximum`. Measure можно передавать в сравнения и
635
- сортировку как expression; DSL разворачивает ссылку в сериализуемое
636
- семантическое выражение до передачи definition в Rust.
637
-
638
- Rust отклоняет агрегаты в обычных graph projections, relation predicates и
639
- dimensions, а также вложенные агрегаты и поля вне объявленных dimensions.
640
- `count` и `countDistinct` возвращают non-null `int64`; остальные меры nullable,
641
- поскольку пустое множество или множество из `NULL` не имеет значения агрегата.
642
- Для сохранения контракта `int64` SQL Server compiler использует `COUNT_BIG`, а
643
- Oracle compiler - `COUNT`. Dimension должен иметь семантику равенства, поэтому
644
- `json` не может задавать идентичность группы.
645
-
646
- Глобальный `DISTINCT` намеренно не выводится из структуры связей. Для количества
647
- уникальных значений используется `countDistinct`, а summary только из dimensions
648
- задает уникальные комбинации через группировку. Произвольный SQL subquery также
649
- не входит в DSL: `exists`, `firstBy` и агрегаты остаются семантическими
650
- операциями, для которых форму подзапроса при необходимости выбирает compiler.
651
-
652
- Summary-план с несколькими независимыми relations `many` отклоняется: обычные
653
- JOIN создали бы декартово размножение веток и могли исказить measures. Ограничение
654
- соседней ветки следует выражать через `exists`. Независимые меры нескольких
655
- коллекций потребуют отдельной aggregate-subquery strategy в planner; compiler не
656
- маскирует эту ситуацию через `DISTINCT`.
657
-
658
- Полученное определение не содержит SQL, имен таблиц, значений конкретного
659
- драйвера, объектов построителя или исполняемых функций обратного вызова
660
- JavaScript. DSL не выполняет планирование запросов: Rust остается единственным
661
- источником правил валидации и компиляции.
662
-
663
- ## Система типов
664
-
665
- При `registerDefinition` Rust выводит тип каждого expression из описаний полей,
666
- параметров и литералов. Проверка выполняется один раз после структурной
667
- валидации и до построения переиспользуемого `QueryGraph`.
668
-
669
- Система типов проверяет:
670
-
671
- - совместимость операндов сравнений;
672
- - строковые аргументы `like`, `lower`, `upper` и `concat`;
673
- - общий тип аргументов `coalesce`;
674
- - boolean-тип условий relations и constraints;
675
- - допустимость сортировки выражений;
676
- - конкретный scalar type и nullability каждого поля проекции;
677
- - типы аргументов агрегатов и их результирующую nullability;
678
- - соответствие неагрегированных выражений объявленным dimensions.
679
-
680
- Поддерживаемые семантические функции задаются DSL-функциями `lower`, `upper`,
681
- `coalesce` и `concat`. Произвольное имя функции не является частью публичного
682
- DSL: сначала функция должна получить общую семантику типов и реализацию для
683
- каждого SQL dialect.
684
-
685
- TypeScript проверяет имена источников, полей и семантических функций во время
686
- написания definition. Rust остается авторитетной проверкой scalar-типов после
687
- композиции модулей; Node.js не обходит граф и не дублирует правила вывода типов.
688
-
689
- `defineGraph` также сохраняет типы параметров и projection paths, включая
690
- содержимое вложенных `GraphModule`. `registerDefinition` переносит эти сведения
691
- на native handle, поэтому операция проверяется непосредственно в месте вызова:
692
-
693
- ```ts
694
- const relationalGraph = registerDefinition(definition).withRelationalMapping(mapping)
695
-
696
- relationalGraph.compileSqlServer({
697
- select: ['id', 'profile.displayName'],
698
- parameters: {
699
- userId: 42,
700
- },
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',
701
154
  })
702
- ```
703
-
704
- TypeScript отклоняет неизвестный projection path или параметр, отсутствие
705
- required-параметра, scalar вместо списка и значение неподходящего scalar type.
706
- Optional-параметры можно не передавать, если они не требуются выбранным планом;
707
- обычно они включают constraint через `when`. Для `int64` допустимы безопасный
708
- JavaScript number и десятичная строка, а list-параметр получает массив того же
709
- типа. Эти проверки существуют только в декларациях TypeScript; сериализация
710
- definition, N-API вызов, runtime-валидация Rust и SQL compilation не меняются.
711
-
712
- ### Тип результата проекции
713
-
714
- DSL сохраняет scalar type и nullability каждого projection expression в compile-time metadata.
715
- `ResultOf` строит из этих сведений вложенный тип результата по projection paths:
716
-
717
- ```ts
718
- import type { QueryOperation, ResultOf } from '@query-graph/core/dsl'
719
-
720
- const operation = {
721
- select: ['id', 'profile.displayName'],
722
- parameters: { userId: 42 },
723
- } as const satisfies QueryOperation<typeof definition>
724
-
725
- type UserRow = ResultOf<typeof definition, typeof operation>
726
- // {
727
- // id: number | string
728
- // profile: { displayName: string | null }
729
- // }
730
- ```
731
-
732
- Если второй аргумент не передан, в результат входят только поля с `default: true`.
733
- `ResultOf` также принимает тип, возвращенный `registerDefinition`, или mapped graph:
734
-
735
- ```ts
736
- type DefaultUserRow = ResultOf<typeof graph>
737
- // { id: number | string }
738
- ```
739
-
740
- Nullability учитывает nullable-поле и всю цепочку optional relations до его source.
741
- Required relation после optional relation не делает значение обязательным. Семантика
742
- выражений тоже сохраняется: например, `coalesce(nullableField, 'fallback')` выводится
743
- как non-null `string`.
744
-
745
- Типы значений по умолчанию нейтральны к DB driver: `int64` и `decimal` представлены
746
- как `number | string`, а `date`, `dateTime` и `binary` - как `string`. Адаптер
747
- исполнения может передать третьим аргументом собственную полную scalar map:
748
-
749
- ```ts
750
- import type { DefaultScalarOutputTypeMap, ResultOf } from '@query-graph/core/dsl'
751
155
 
752
- type DriverScalars = Omit<DefaultScalarOutputTypeMap, 'int64' | 'dateTime'> & {
753
- int64: bigint
754
- dateTime: Date
755
- }
756
-
757
- type DriverUserRow = ResultOf<typeof definition, typeof operation, DriverScalars>
156
+ const mappedOrders = registerDefinition(ordersDefinition).withRelationalMapping(orderMapping)
157
+ const ordersGraph = composeGraph({ root: mappedOrders, relations: [items] })
758
158
  ```
759
159
 
760
- Для точного учета явного `select` operation должна сохранять literal tuple через
761
- `as const` или `satisfies`. Это только TypeScript API: wire definition, N-API и
762
- Rust planner не получают дополнительного runtime state.
763
-
764
- `ResultOf` описывает логическую форму одной строки, восстановленную из путей
765
- проекции. Он не выполняет запрос и не превращает relation `many` в массив.
766
- Группировка строк, дедупликация и гидратация коллекций остаются ответственностью
767
- потребителя модуля.
768
-
769
- ## Диагностика
160
+ Обычные source и projection declarations здесь свернуты в именованные
161
+ константы. <code>customerObject</code> и <code>latestPaymentObject</code> созданы
162
+ через <code>projectObject</code>, поэтому отсутствующая to-one строка становится
163
+ <code>null</code>.
770
164
 
771
- Ошибки N-API имеют имя `QueryGraphError` и сохраняют обычный `message` и stack.
772
- Для программной обработки доступны:
773
-
774
- - `code` — категория ошибки на границе пакета;
775
- - `phase` — `definition`, `mapping`, `operation` или `sql`;
776
- - `issues` — массив `{ code, location, message }`.
165
+ Одна и та же definition теперь обслуживает дешевый список:
777
166
 
778
167
  ```ts
779
- import type { QueryGraphError } from '@query-graph/core/definition'
780
-
781
- try {
782
- relationalGraph.compileSqlServer(operation)
783
- } catch (cause) {
784
- const error = cause as QueryGraphError
785
-
786
- if (error.name === 'QueryGraphError') {
787
- console.table(error.issues)
788
- }
789
- }
790
- ```
791
-
792
- Wire errors, semantic validation и SQL compilation имеют разные верхнеуровневые
793
- `code`. Коды отдельных `issues` стабильны и не требуют разбора текста `message`.
794
-
795
- ## Presence объектной проекции
796
-
797
- Nullable-поля не позволяют определить, отсутствует ли вложенный объект: существующий
798
- объект может корректно содержать только `NULL`. Для такой границы definition явно
799
- задаёт выражение присутствия через `projectObject`:
800
-
801
- ```ts
802
- const definition = defineGraph({
803
- name: 'users',
804
- root: users,
805
- sources: [users, profiles],
806
- relations: [
807
- relation({
808
- name: 'profile',
809
- from: users,
810
- to: profiles,
811
- on: eq(users.field('id'), profiles.field('userId')),
812
- }),
813
- ],
814
- objects: [
815
- projectObject({
816
- path: 'profile',
817
- presence: profiles.field('id'),
818
- }),
819
- ],
820
- projection: [
821
- project({
822
- path: 'profile.displayName',
823
- expression: profiles.field('displayName'),
824
- }),
825
- ],
168
+ const listPlan = ordersGraph.compileSqlServerPlan({
169
+ select: ['id'],
170
+ parameters: { organisationId: 42 },
826
171
  })
827
- ```
828
-
829
- `presence` должно возвращать не-`NULL`, когда объект существует, и `NULL`, когда
830
- его нет. Обычно для этого используется обязательный идентификатор source за
831
- optional relation. Маркер не является доступным для выбора полем проекции.
832
-
833
- Если operation выбирает хотя бы одно поле под `profile`, planner включает
834
- presence-выражение в план. SQL compiler добавляет скрытую колонку, например `o0`,
835
- и возвращает отдельные metadata:
836
172
 
837
- ```ts
838
- statement.objects
839
- // [{ path: 'profile', presenceColumn: 'o0' }]
173
+ listPlan.root.relations // []
174
+ listPlan.batches // []
840
175
  ```
841
176
 
842
- `statement.columns` по-прежнему содержит только публичные поля `c0`, `c1` и так
843
- далее. Исполнитель сначала восстанавливает результат по `statement.columns`, а
844
- затем устанавливает объект по `path` в `null`, если соответствующая исходная
845
- колонка `presenceColumn` равна `NULL`. Для вложенных объектов metadata упорядочены
846
- от самого глубокого path к внешнему.
847
-
848
- Таким образом, исполнитель не угадывает отсутствие объекта по правилу «все поля
849
- равны `NULL`». `ResultOf` также учитывает declaration и выводит
850
- `profile: { ... } | null`. Пакет только компилирует SQL и metadata; выполнение
851
- запроса и materialization остаются у потребителя.
852
-
853
- ## SQL Server
854
-
855
- Реляционное отображение связывает логические источники с физическими таблицами.
856
- По умолчанию логические имена полей используются как имена колонок. В `columns`
857
- указываются только переопределения.
177
+ И подробное представление:
858
178
 
859
179
  ```ts
860
- const relationalGraph = graph.withRelationalMapping({
861
- sources: {
862
- users: {
863
- table: {
864
- schema: 'dbo',
865
- name: 'users',
866
- },
867
- },
868
- profiles: {
869
- table: {
870
- schema: 'dbo',
871
- name: 'user_profiles',
872
- },
873
- columns: {
874
- userId: 'user_id',
875
- displayName: 'display_name',
876
- },
877
- },
878
- },
180
+ const detailsPlan = ordersGraph.compileSqlServerPlan({
181
+ select: ['id', 'customer.name', 'latestPayment.amount', 'items.name'],
182
+ parameters: { organisationId: 42 },
879
183
  })
880
- ```
881
-
882
- `withRelationalMappings([...])` является формой композиции того же mapping и
883
- возвращает такой же типизированный `RelationalQueryGraph`.
884
-
885
- Операция передается в Rust одним вызовом. Компилятор выбирает необходимые пути
886
- связей, формирует синтаксис SQL Server и возвращает описания параметров.
887
-
888
- ```ts
889
- const operation = {
890
- select: ['id', 'profile.displayName'],
891
- parameters: {
892
- userId: 42,
893
- },
894
- offset: 0,
895
- limit: 25,
896
- }
897
-
898
- const statement = relationalGraph.compileSqlServer(operation)
899
-
900
- console.log(statement.sql)
901
- console.table(statement.bindings)
902
- console.table(statement.columns)
903
- console.table(statement.relations)
904
- ```
905
184
 
906
- `statement.bindings` связывает сгенерированные имена, например `p0`, с
907
- логическими параметрами, например `userId`, и содержит их scalar type.
908
- Потребитель берет значения из `operation.parameters` и передает их
909
- своему драйверу базы данных.
185
+ detailsPlan.root.relations.map(({ name }) => name)
186
+ // ['customer', 'latestPayment']
910
187
 
911
- Wire-контракт значений параметров:
912
-
913
- | Scalar type | Значение в `operation.parameters` |
914
- | ----------- | -------------------------------------------------------------- |
915
- | `boolean` | `boolean` |
916
- | `int32` | целое JavaScript number в диапазоне `int32` |
917
- | `int64` | безопасное целое JavaScript number или десятичная строка `i64` |
918
- | `float64` | JSON number |
919
- | `decimal` | JSON number или десятичная строка без экспоненты |
920
- | `string` | string |
921
- | `date` | непрозрачная для compiler строка |
922
- | `dateTime` | непрозрачная для compiler строка |
923
- | `binary` | непрозрачная для compiler строка |
924
- | `json` | любое JSON-совместимое значение |
925
-
926
- Optional parameter означает отсутствие ключа, а не `null`. Формат строк
927
- `date`, `dateTime` и `binary`, а также преобразование scalar type в тип конкретного
928
- DB driver определяет adapter исполнения. SQL compiler не копирует значения в
929
- `statement`: binding metadata ссылается на исходный ключ параметра.
930
-
931
- Целевую версию можно указать отдельно от operation:
932
-
933
- ```ts
934
- const statement = relationalGraph.compileSqlServer(operation, { version: '2019' })
188
+ detailsPlan.batches.map(({ name }) => name)
189
+ // ['items']
935
190
  ```
936
191
 
937
- По умолчанию используется SQL Server 2012. SQL Server 2008 поддерживается для
938
- запросов без `OFFSET/FETCH`; несовместимая pagination отклоняется до исполнения.
939
-
940
- Компилятор намеренно не использует логические имена графа как SQL-алиасы.
941
- Источники получают короткие физические алиасы `t0`, `t1`, а выходные колонки -
942
- `c0`, `c1`. Поэтому длинные имена источников и пути проекции не зависят от
943
- ограничений конкретной СУБД на длину идентификаторов.
944
-
945
- `statement.columns` сопоставляет физические имена колонок с логическими путями
946
- проекции, содержит их `scalarType`, `nullable` и цепочки relations.
947
- `statement.relations` описывает выбранный планировщиком путь связей, включая
948
- направление, cardinality и фактическую обязательность JOIN. Эти metadata позволяют
949
- потребителю сопоставить плоские SQL-колонки с логическими путями, не разбирая SQL.
950
- Группировка строк и гидратация коллекций не входят в контракт пакета и остаются
951
- ответственностью потребителя.
952
-
953
- ## Oracle
954
-
955
- Oracle использует тот же planner и operation. Отличается только финальный
956
- dialect renderer:
192
+ Planner не добавляет невыбранную <code>customerOrganisation</code>,
193
+ <code>exists(activePromotion)</code> остается semijoin без размножения root-строк,
194
+ а <code>items</code> загружается одним batch-запросом для всех найденных orders.
957
195
 
958
- ```ts
959
- const statement = relationalGraph.compileOracle(operation)
960
- const oracle19cStatement = relationalGraph.compileOracle(operation, { version: '19c' })
196
+ ## Модель
961
197
 
962
- console.log(statement.sql)
963
- console.table(statement.bindings)
198
+ ```text
199
+ defineGraph
200
+ |
201
+ v
202
+ registerDefinition Rust validation + reusable analysis
203
+ |
204
+ v
205
+ withRelationalMapping logical sources -> physical tables
206
+ |
207
+ +---- QueryOperation
208
+ | |
209
+ v v
210
+ compileSqlServer / compileOracle
211
+ |
212
+ v
213
+ CompiledSqlStatement SQL + bindings + result metadata
214
+ |
215
+ v
216
+ application executor DB connection remains outside query-graph
964
217
  ```
965
218
 
966
- Компилятор генерирует именованные параметры `:p0`, Oracle-кавычки идентификаторов,
967
- нативный порядок `NULLS FIRST`/`NULLS LAST`, конкатенацию через `||` и пагинацию
968
- `OFFSET`/`FETCH`, доступную начиная с Oracle 12c. `catalog` в `TableName` относится
969
- к SQL Server и отклоняется Oracle-компилятором; имя схемы поддерживается.
970
-
971
- По умолчанию выбирается Oracle 12c. Oracle 11g поддерживается для запросов без
972
- pagination; `OFFSET/FETCH` для него отклоняется как неподдерживаемая capability.
973
-
974
- Оба компилятора поддерживают выбор полей проекции, scalar/list параметры,
975
- ограничения определения,
976
- условные ограничения, сортировку по умолчанию, соединения
977
- `INNER JOIN`/`LEFT JOIN`, summary dimensions/measures и пагинацию
978
- `OFFSET`/`FETCH`. Общий SQL pipeline выбирает пути связей, фазы `WHERE`/`HAVING`
979
- и обходит expression AST, а dialect renderer отвечает только за синтаксис
980
- конкретной СУБД.
981
-
982
- Для обычного record-графа пагинация отклоняется, если план проходит через
983
- relation с cardinality `many`: JOIN в таком случае меняет количество корневых
984
- строк. Summary-граф может пагинировать результат после группировки, поэтому
985
- relation `many` для него допустим. Для пагинации вложенных record-коллекций
986
- по-прежнему понадобится отдельный split-query plan.
219
+ | Понятие | Ответственность |
220
+ | --------------------------------- | ------------------------------------------------------------------ |
221
+ | <code>GraphDefinition</code> | sources, parameters, relations, constraints, projection и ordering |
222
+ | <code>GraphModule</code> | переиспользуемая часть definition без собственного root |
223
+ | <code>QueryGraph</code> | проверенный и проиндексированный нативный граф |
224
+ | <code>RelationalMapping</code> | таблицы, схемы и физические имена колонок |
225
+ | <code>QueryOperation</code> | select, parameters, ordering и pagination одного запроса |
226
+ | <code>CompiledSqlStatement</code> | SQL и контракт результата для executor |
987
227
 
988
- Фильтры времени выполнения и пользовательские отображения семантических функций
989
- намеренно оставлены для последующих этапов.
228
+ ## Куда дальше
990
229
 
991
- Полный пример графа, отображения, операции, сгенерированного SQL и замера
992
- производительности компиляции находится в `benchmark/bench.ts`.
230
+ - [Authoring DSL](docs/authoring.md): sources, expressions, relations,
231
+ constraints, projections, ordering и summary.
232
+ - [Composition](docs/composition.md): GraphModule, batchQuery, batchRelation и
233
+ двухфазное выполнение.
234
+ - [Type system](docs/type-system.md): scalar types, nullability,
235
+ <code>QueryOperation</code> и <code>ResultOf</code>.
236
+ - [SQL compilation](docs/sql.md): relational mapping, dialect versions,
237
+ bindings и контракт executor.
238
+ - [Diagnostics](docs/diagnostics.md): фазы ошибок, коды и границы runtime
239
+ validation.
993
240
 
994
- ## Batch-связи и двухфазное выполнение
241
+ ## Entry Points
995
242
 
996
- Поля из отдельного запроса подключаются без изменения обычного relational-графа:
243
+ Канонический API:
997
244
 
998
245
  ```ts
999
- const childrenQuery = batchQuery({
1000
- graph: childrenGraph,
1001
- key: {
1002
- path: 'parentId',
1003
- parameter: parentIds,
1004
- },
1005
- })
1006
-
1007
- const children = batchRelation({
1008
- name: 'children',
1009
- from: 'id',
1010
- query: childrenQuery,
1011
- cardinality: 'many',
1012
- })
1013
-
1014
- const graph = composeGraph({ root: rootGraph, relations: [children] })
1015
- const plan = graph.compileOraclePlan({
1016
- select: ['id', 'children.id'],
1017
- parameters: {},
1018
- limit: 20,
1019
- })
246
+ import { registerDefinition } from '@query-graph/core'
247
+ import { defineGraph, relation, project } from '@query-graph/core/dsl'
1020
248
  ```
1021
249
 
1022
- `batchQuery` принадлежит child-модулю и один раз закрепляет его graph,
1023
- projection path ключа и list-параметр. Поэтому разные root-графы могут
1024
- переиспользовать query, не повторяя внутренний контракт child-запроса.
250
+ <code>@query-graph/core/definition</code> сохраняет позиционные фабрики только
251
+ для обратной совместимости с ранним API 1.x.
252
+ <code>@query-graph/dsl-object</code> остается тонким deprecated facade над
253
+ <code>@query-graph/core/dsl</code>. Новому коду эти entrypoint не нужны.
1025
254
 
1026
- `composeGraph` оставляет в JavaScript только декларативный фасад. Проверка
1027
- совместимости ключей и параметров, разделение operation на root и batch-шаги и
1028
- компиляция SQL выполняются в Rust. `compileOraclePlan` или
1029
- `compileSqlServerPlan` сразу компилирует только `plan.root`; child SQL создаётся
1030
- при вызове `compileBatch`. Plan сохраняет выбранные dialect и version, поэтому
1031
- отложенный statement компилируется с теми же настройками, что и root.
255
+ Batch-фабрики временно доступны из package root, но новый код должен импортировать
256
+ их из <code>@query-graph/core/dsl</code>.
1032
257
 
1033
- Для подготовки child bindings каждый шаг в `plan.batches` также содержит имя
1034
- ключевого параметра `keyParameter` и снимок статических `parameters`. Executor
1035
- объединяет их с `{ [batch.keyParameter]: keys }` и сопоставляет полученные значения
1036
- с возвращёнными `statement.bindings`.
258
+ ## Архитектура
1037
259
 
1038
- Executor потребителя выполняет `plan.root`, собирает уникальные ненулевые
1039
- значения `parentKey` из `plan.batches`, вызывает
1040
- `plan.compileBatch('children', parentIds)`, выполняет child statement и
1041
- связывает строки по `childKey`. `cardinality: 'one'` означает объект или `null`,
1042
- а `many` — массив. Query-graph не подключается к БД, не дедуплицирует ключи, не
1043
- выполняет statements и не гидратирует результат; при пустом наборе ключей
1044
- executor должен пропустить child-запрос.
1045
-
1046
- Ordering, offset и limit корневой operation применяются только к `plan.root`.
1047
- Batch-связь может задать собственный `ordering`, но child pagination не
1048
- добавляется. Batch-поля выбираются только явно (`children.id`); без prefixed
1049
- path соответствующего шага в `plan.batches` нет. Флаги injection в metadata
1050
- показывают executor, какие ключи были добавлены исключительно для связывания и
1051
- не должны попасть в публичный результат.
1052
-
1053
- ---
1054
-
1055
- ### Rule 34
260
+ <code>query-graph-core</code> не зависит от Node.js. В нем находятся wire model,
261
+ analysis, type checker, planner, relational mapping и SQL compilers. Корневой
262
+ Rust crate содержит только N-API adapter и перевод diagnostics/metadata в
263
+ JavaScript.
1056
264
 
1057
- > Every sufficiently generic abstraction eventually gets an implementation.
265
+ SQL execution намеренно остается у потребителя. Это позволяет использовать
266
+ любой pool, transaction model, tracing и политику retry, не связывая planner с
267
+ конкретным DB driver.