@query-graph/core 1.1.3 → 1.1.5

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