@query-graph/core 1.0.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 N-API for Rust
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,620 @@
1
+ # query-graph
2
+
3
+ Нативное планирование графов запросов и компиляция SQL для Node.js.
4
+
5
+ Пакет отвечает за валидацию графа, планирование и генерацию SQL. Подключение к
6
+ базе данных и исполнение запросов остаются ответственностью потребителя.
7
+ Один и тот же граф и реляционное отображение могут компилироваться в SQL Server
8
+ или Oracle SQL.
9
+
10
+ ## Архитектура пакета
11
+
12
+ Cargo workspace разделён на два слоя. `query-graph-core` не зависит от Node.js
13
+ или N-API и содержит definition, type system, planner, relational mapping и SQL
14
+ compilers. Корневой crate `query_graph` является тонким N-API adapter: он
15
+ десериализует wire-объекты, вызывает core и переводит результат и diagnostics в
16
+ JavaScript.
17
+
18
+ Зависимость направлена только от adapter к core. Исполнение SQL в оба слоя не входит.
19
+
20
+ ## Определение графа
21
+
22
+ Определение графа не зависит от диалекта SQL и содержит:
23
+
24
+ - логические источники и поля;
25
+ - типизированные параметры;
26
+ - именованные связи;
27
+ - семантические ограничения;
28
+ - выходную проекцию;
29
+ - именованные варианты сортировки.
30
+
31
+ `registerDefinition` один раз передает определение в Rust. Rust валидирует все
32
+ ссылки, создает переиспользуемые индексы и возвращает нативный дескриптор
33
+ `QueryGraph`.
34
+
35
+ ```ts
36
+ import { registerDefinition } from '@query-graph/core'
37
+ import {
38
+ asc,
39
+ constraint,
40
+ desc,
41
+ defineGraph,
42
+ defineGraphModule,
43
+ eq,
44
+ exists,
45
+ firstBy,
46
+ inParameter,
47
+ isNotNull,
48
+ nullable,
49
+ ordering,
50
+ param,
51
+ project,
52
+ relation,
53
+ requiredListParameter,
54
+ requiredParameter,
55
+ source,
56
+ } from '@query-graph/core/dsl'
57
+
58
+ const link = source('link', {
59
+ idOwner: 'int64',
60
+ idControllerObjectValue: 'int64',
61
+ })
62
+
63
+ const value = source('value', {
64
+ id: 'int64',
65
+ value: nullable('string'),
66
+ })
67
+
68
+ const idOwner = requiredParameter('idOwner', 'int64')
69
+ const valueRelation = relation({
70
+ name: 'value',
71
+ from: link,
72
+ to: value,
73
+ on: eq(link.field('idControllerObjectValue'), value.field('id')),
74
+ required: true,
75
+ })
76
+
77
+ const attributeValuesModule = defineGraphModule({
78
+ name: 'attributeValues',
79
+ sources: [link, value],
80
+ parameters: [idOwner],
81
+ relations: [valueRelation],
82
+ constraints: [
83
+ constraint({
84
+ predicate: eq(link.field('idOwner'), param(idOwner)),
85
+ }),
86
+ constraint({
87
+ predicate: exists(value, isNotNull(value.field('value'))),
88
+ }),
89
+ ],
90
+ projection: [
91
+ project({
92
+ path: 'value.value',
93
+ expression: value.field('value'),
94
+ default: true,
95
+ }),
96
+ ],
97
+ })
98
+
99
+ const definition = defineGraph({
100
+ name: 'attributeValues',
101
+ root: link,
102
+ modules: [attributeValuesModule],
103
+ })
104
+
105
+ const graph = registerDefinition(definition)
106
+ ```
107
+
108
+ `GraphModule` группирует переиспользуемую часть определения: источники,
109
+ параметры, связи, ограничения, проекцию и сортировку. У модуля нет собственного
110
+ корневого источника, и его нельзя зарегистрировать или скомпилировать отдельно.
111
+
112
+ `defineGraph` объединяет вложенные модули и локальные элементы в один плоский
113
+ `GraphDefinition`. Повторно подключенный объект дедуплицируется по identity. Для
114
+ именованных элементов разные определения с одним ключом приводят к ошибке
115
+ композиции.
116
+
117
+ Constraints не имеют имени: повторно использованный constraint добавляется один
118
+ раз, а разные constraint-объекты сохраняются и объединяются через `AND`. Сведения
119
+ о модулях не передаются в Rust и не входят в wire-формат.
120
+
121
+ Вспомогательные функции API описания графа формируют версионируемое и
122
+ сериализуемое промежуточное представление `GraphDefinition`. Дискриминаторы
123
+ выражений, например `kind: 'eq'`, существуют только в сгенерированном
124
+ представлении. В прикладном коде графа их писать не требуется. TypeScript
125
+ проверяет ссылки на поля источников.
126
+
127
+ Relation path проекции указывать не требуется. При регистрации definition Rust
128
+ находит самый глубокий source, использованный expression, и один раз сохраняет
129
+ путь от root до этого source. Expression может обращаться к root и любым sources
130
+ на одной ветке. Обращение к двум независимым веткам отклоняется как неоднозначная
131
+ проекция. Planner использует уже вычисленные пути и не повторяет inference при
132
+ каждой компиляции operation.
133
+
134
+ ### Варианты DSL
135
+
136
+ Canonical `GraphDefinition` не зависит от authoring DSL. Основной authoring API
137
+ доступен через `@query-graph/core/dsl` и использует объектные конфигурации. Позиционный
138
+ `@query-graph/core/definition` сохранён как адаптер совместимости для существующего кода;
139
+ новые графы на него ориентировать не следует.
140
+
141
+ Отдельный пакет `@query-graph/dsl-object` является тонким фасадом над тем же API.
142
+ Он полезен, когда DSL нужен как отдельная зависимость:
143
+
144
+ ```bash
145
+ npm install @query-graph/core @query-graph/dsl-object
146
+ ```
147
+
148
+ ```ts
149
+ import { registerDefinition } from '@query-graph/core'
150
+ import { asc, defineGraph, eq, firstBy, project, relation, source } from '@query-graph/dsl-object'
151
+
152
+ const staff = source('staff', { id: 'int64' })
153
+ const personStaff = source('personStaff', {
154
+ id: 'int64',
155
+ idStaff: 'int64',
156
+ idPerson: 'int64',
157
+ })
158
+
159
+ const definition = defineGraph({
160
+ name: 'staff',
161
+ root: staff,
162
+ sources: [staff, personStaff],
163
+ relations: [
164
+ relation({
165
+ name: 'credentials',
166
+ from: staff,
167
+ to: personStaff,
168
+ on: eq(staff.field('id'), personStaff.field('idStaff')),
169
+ cardinality: 'one',
170
+ selection: firstBy(asc(personStaff.field('idPerson')), asc(personStaff.field('id'))),
171
+ }),
172
+ ],
173
+ projection: [
174
+ project({ path: 'id', expression: staff.field('id'), default: true }),
175
+ project({ path: 'credentials.idPerson', expression: personStaff.field('idPerson') }),
176
+ ],
177
+ })
178
+
179
+ const graph = registerDefinition(definition)
180
+ ```
181
+
182
+ Фасад не содержит второй реализации и напрямую переэкспортирует
183
+ `@query-graph/core/dsl`. Расширение позиционного compatibility API не меняет поверхность
184
+ объектного DSL.
185
+
186
+ Оба входа формируют один и тот же versioned wire-контракт. Сторонний DSL также может
187
+ создавать `GraphDefinitionInput`; Rust проверяет его при `registerDefinition`.
188
+
189
+ ### Existential constraints
190
+
191
+ `exists(source, predicate?)` задаёт семантический квантор внутри constraint.
192
+ Указывать SQL-подзапрос или correlation columns не требуется: Rust использует
193
+ уникальный relation path от root до указанного source.
194
+
195
+ ```ts
196
+ constraint({
197
+ predicate: exists(businessServiceStaff, eq(businessServiceStaff.field('idService'), param(idService))),
198
+ })
199
+ ```
200
+
201
+ Predicate может обращаться к root и sources на выведенном пути. Ссылки на
202
+ соседнюю ветку отклоняются при регистрации definition. `exists` вне constraints
203
+ также отклоняется; отрицание выражается обычным `not(exists(...))`.
204
+
205
+ Planner воспринимает existential branch как semijoin. Sources этой ветки не
206
+ добавляются во внешние JOIN и `statement.relations`, поэтому relation с
207
+ cardinality `many` не размножает корневые строки и не мешает их пагинации.
208
+ Oracle и SQL Server compiler строят коррелированный `EXISTS` из одной и той же
209
+ семантики definition.
210
+
211
+ По умолчанию `exists` коррелируется от root. Для semijoin относительно уже
212
+ выбранного внешнего source можно явно указать точку корреляции:
213
+
214
+ ```ts
215
+ constraint({
216
+ predicate: exists(categoryLinks, categoryPredicate, { from: attributeValues }),
217
+ })
218
+ ```
219
+
220
+ `from` должен быть строгим предком целевого source на relation path. Planner
221
+ оставляет путь до `from` во внешнем запросе, а оставшийся путь компилирует как
222
+ коррелированный semijoin.
223
+
224
+ ### Параметры-списки
225
+
226
+ Параметр имеет явную форму `scalar` или `list`. Один параметр не меняет форму
227
+ между операциями, поэтому в прикладном коде не требуется поддерживать
228
+ `number | number[]`.
229
+
230
+ ```ts
231
+ const idServices = requiredListParameter('idServices', 'int64')
232
+
233
+ const definition = defineGraph({
234
+ name: 'staffByServices',
235
+ root: staff,
236
+ sources: [staff, businessServiceStaff],
237
+ parameters: [idServices],
238
+ relations: [
239
+ relation({
240
+ name: 'staffServices',
241
+ from: staff,
242
+ to: businessServiceStaff,
243
+ on: eq(staff.field('id'), businessServiceStaff.field('idStaff')),
244
+ cardinality: 'many',
245
+ }),
246
+ ],
247
+ constraints: [
248
+ constraint({
249
+ predicate: exists(businessServiceStaff, inParameter(businessServiceStaff.field('idService'), idServices)),
250
+ }),
251
+ ],
252
+ projection: [project({ path: 'id', expression: staff.field('id'), default: true })],
253
+ })
254
+ ```
255
+
256
+ Operation передает обычный массив:
257
+
258
+ ```ts
259
+ const statement = relationalGraph.compileSqlServer({
260
+ parameters: { idServices: [12, 18, 24] },
261
+ })
262
+ ```
263
+
264
+ Compiler разворачивает его в `IN (@p0, @p1, @p2)`. Каждый элемент получает
265
+ binding `{ parameter: 'idServices', index, scalarType: 'int64' }`, по которому
266
+ adapter исполнения берет значение из исходного массива. Rust проверяет каждый
267
+ элемент отдельно и возвращает путь ошибки наподобие
268
+ `parameters.idServices[2]`.
269
+
270
+ Отсутствующий optional list отключает constraint с `when`. Переданный пустой
271
+ список остается присутствующим параметром и компилируется в ложный предикат
272
+ `1 = 0`; недопустимый SQL `IN ()` не создается.
273
+
274
+ ### Именованные сортировки
275
+
276
+ Definition объявляет допустимые способы упорядочивания, а operation выбирает один из них по имени:
277
+
278
+ ```ts
279
+ const definition = defineGraph({
280
+ // ...
281
+ orderings: [
282
+ ordering({ name: 'createdDesc', by: [desc(items.field('dateCreate'))], default: true }),
283
+ ordering({ name: 'nameAsc', by: [asc(items.field('name'))] }),
284
+ ],
285
+ })
286
+
287
+ relationalGraph.compileOracle({
288
+ ordering: 'nameAsc',
289
+ parameters,
290
+ })
291
+ ```
292
+
293
+ Поле `default: true` можно указать только у одного варианта. Если operation не передает
294
+ `ordering`, planner использует default-вариант; без него SQL строится без `ORDER BY`.
295
+ TypeScript выводит union имен из definition и подключенных graph modules. Rust проверяет имя
296
+ повторно и планирует только связи и параметры выбранной сортировки.
297
+
298
+ ### To-one selection
299
+
300
+ `cardinality: 'one'` описывает контракт связи. Если физический источник может
301
+ содержать несколько кандидатов, `firstBy` задает семантическое правило выбора
302
+ одной строки:
303
+
304
+ ```ts
305
+ const credentials = relation({
306
+ name: 'credentials',
307
+ from: staff,
308
+ to: personStaff,
309
+ on: eq(staff.field('id'), personStaff.field('idStaff')),
310
+ selection: firstBy(asc(personStaff.field('idPerson')), asc(personStaff.field('id'))),
311
+ })
312
+ ```
313
+
314
+ Поля сортировки могут обращаться только к target source. Список сортировки
315
+ обязателен; последнее поле следует выбирать уникальным, чтобы результат был
316
+ детерминированным. `required: false` означает `0..1`, а `required: true` -
317
+ ровно одну связанную строку.
318
+
319
+ SQL Server renderer использует `OUTER/CROSS APPLY` и `TOP (1)`, Oracle 12c+
320
+ использует `OUTER/CROSS APPLY` и `FETCH FIRST 1 ROW ONLY`. Эти детали не входят
321
+ в definition. Для Oracle 11g `firstBy` возвращает
322
+ `unsupportedDialectFeature`.
323
+
324
+ ### Summary-графы
325
+
326
+ `defineSummaryGraph` описывает результат через dimensions и measures, не через
327
+ SQL-конструкции `GROUP BY` и `HAVING`:
328
+
329
+ ```ts
330
+ const serviceId = dimension({
331
+ path: 'serviceId',
332
+ expression: service.field('id'),
333
+ default: true,
334
+ })
335
+
336
+ const staffCount = measure({
337
+ path: 'staffCount',
338
+ expression: countDistinct(serviceStaff.field('idStaff')),
339
+ default: true,
340
+ })
341
+
342
+ const definition = defineSummaryGraph({
343
+ name: 'serviceSummary',
344
+ root: service,
345
+ sources: [service, serviceStaff],
346
+ parameters: [idOrganisation, minimumStaff],
347
+ relations: [
348
+ relation({
349
+ name: 'staff',
350
+ from: service,
351
+ to: serviceStaff,
352
+ on: eq(service.field('id'), serviceStaff.field('idService')),
353
+ cardinality: 'many',
354
+ }),
355
+ ],
356
+ constraints: [
357
+ constraint({
358
+ predicate: eq(service.field('idOrganisation'), param(idOrganisation)),
359
+ }),
360
+ constraint({
361
+ predicate: gte(staffCount, param(minimumStaff)),
362
+ }),
363
+ ],
364
+ dimensions: [serviceId],
365
+ measures: [staffCount],
366
+ orderings: [
367
+ ordering({
368
+ name: 'staffCountDesc',
369
+ by: [desc(staffCount)],
370
+ default: true,
371
+ }),
372
+ ],
373
+ })
374
+ ```
375
+
376
+ Planner включает все объявленные dimensions в идентичность результата и
377
+ компилирует их в `GROUP BY`, даже если operation не возвращает часть из них.
378
+ Constraint без агрегата фильтрует исходное множество через `WHERE`. Constraint
379
+ с агрегатом фильтрует уже построенные группы через `HAVING`. Поэтому автор графа
380
+ не выбирает SQL-фазу вручную и не может случайно поместить агрегат в `WHERE`.
381
+
382
+ Поддерживаются `count()`, `count(expression)`, `countDistinct`, `sum`,
383
+ `average`, `minimum` и `maximum`. Measure можно передавать в сравнения и
384
+ сортировку как expression; DSL разворачивает ссылку в сериализуемое
385
+ семантическое выражение до передачи definition в Rust.
386
+
387
+ Rust отклоняет агрегаты в обычных graph projections, relation predicates и
388
+ dimensions, а также вложенные агрегаты и поля вне объявленных dimensions.
389
+ `count` и `countDistinct` возвращают non-null `int64`; остальные меры nullable,
390
+ поскольку пустое множество или множество из `NULL` не имеет значения агрегата.
391
+ Для сохранения контракта `int64` SQL Server compiler использует `COUNT_BIG`, а
392
+ Oracle compiler - `COUNT`. Dimension должен иметь семантику равенства, поэтому
393
+ `json` не может задавать идентичность группы.
394
+
395
+ Глобальный `DISTINCT` намеренно не выводится из структуры связей. Для количества
396
+ уникальных значений используется `countDistinct`, а summary только из dimensions
397
+ задает уникальные комбинации через группировку. Произвольный SQL subquery также
398
+ не входит в DSL: `exists`, `firstBy` и агрегаты остаются семантическими
399
+ операциями, для которых форму подзапроса при необходимости выбирает compiler.
400
+
401
+ Summary-план с несколькими независимыми relations `many` отклоняется: обычные
402
+ JOIN создали бы декартово размножение веток и могли исказить measures. Ограничение
403
+ соседней ветки следует выражать через `exists`. Независимые меры нескольких
404
+ коллекций потребуют отдельной aggregate-subquery strategy в planner; compiler не
405
+ маскирует эту ситуацию через `DISTINCT`.
406
+
407
+ Полученное определение не содержит SQL, имен таблиц, значений конкретного
408
+ драйвера, объектов построителя или исполняемых функций обратного вызова
409
+ JavaScript. DSL не выполняет планирование запросов: Rust остается единственным
410
+ источником правил валидации и компиляции.
411
+
412
+ ## Система типов
413
+
414
+ При `registerDefinition` Rust выводит тип каждого expression из описаний полей,
415
+ параметров и литералов. Проверка выполняется один раз после структурной
416
+ валидации и до построения переиспользуемого `QueryGraph`.
417
+
418
+ Система типов проверяет:
419
+
420
+ - совместимость операндов сравнений;
421
+ - строковые аргументы `like`, `lower`, `upper` и `concat`;
422
+ - общий тип аргументов `coalesce`;
423
+ - boolean-тип условий relations и constraints;
424
+ - допустимость сортировки выражений;
425
+ - конкретный scalar type и nullability каждого поля проекции;
426
+ - типы аргументов агрегатов и их результирующую nullability;
427
+ - соответствие неагрегированных выражений объявленным dimensions.
428
+
429
+ Поддерживаемые семантические функции задаются DSL-функциями `lower`, `upper`,
430
+ `coalesce` и `concat`. Произвольное имя функции не является частью публичного
431
+ DSL: сначала функция должна получить общую семантику типов и реализацию для
432
+ каждого SQL dialect.
433
+
434
+ TypeScript проверяет имена источников, полей и семантических функций во время
435
+ написания definition. Rust остается авторитетной проверкой scalar-типов после
436
+ композиции модулей; Node.js не обходит граф и не дублирует правила вывода типов.
437
+
438
+ `defineGraph` также сохраняет типы параметров и projection paths, включая
439
+ содержимое вложенных `GraphModule`. `registerDefinition` переносит эти сведения
440
+ на native handle, поэтому операция проверяется непосредственно в месте вызова:
441
+
442
+ ```ts
443
+ const relationalGraph = registerDefinition(definition).withRelationalMapping(mapping)
444
+
445
+ relationalGraph.compileSqlServer({
446
+ select: ['id', 'credentials.idPerson'],
447
+ parameters: {
448
+ idOrganisation: 42,
449
+ personIds: [7, 11],
450
+ },
451
+ })
452
+ ```
453
+
454
+ TypeScript отклоняет неизвестный projection path или параметр, отсутствие
455
+ required-параметра, scalar вместо списка и значение неподходящего scalar type.
456
+ Optional-параметры можно не передавать, если они не требуются выбранным планом;
457
+ обычно они включают constraint через `when`. Для `int64` допустимы безопасный
458
+ JavaScript number и десятичная строка, а list-параметр получает массив того же
459
+ типа. Эти проверки существуют только в декларациях TypeScript; сериализация
460
+ definition, N-API вызов, runtime-валидация Rust и SQL compilation не меняются.
461
+
462
+ ## Диагностика
463
+
464
+ Ошибки N-API имеют имя `QueryGraphError` и сохраняют обычный `message` и stack.
465
+ Для программной обработки доступны:
466
+
467
+ - `code` — категория ошибки на границе пакета;
468
+ - `phase` — `definition`, `mapping`, `operation` или `sql`;
469
+ - `issues` — массив `{ code, location, message }`.
470
+
471
+ ```ts
472
+ import type { QueryGraphError } from '@query-graph/core/definition'
473
+
474
+ try {
475
+ relationalGraph.compileSqlServer(operation)
476
+ } catch (cause) {
477
+ const error = cause as QueryGraphError
478
+
479
+ if (error.name === 'QueryGraphError') {
480
+ console.table(error.issues)
481
+ }
482
+ }
483
+ ```
484
+
485
+ Wire errors, semantic validation и SQL compilation имеют разные верхнеуровневые
486
+ `code`. Коды отдельных `issues` стабильны и не требуют разбора текста `message`.
487
+
488
+ ## SQL Server
489
+
490
+ Реляционное отображение связывает логические источники с физическими таблицами.
491
+ По умолчанию логические имена полей используются как имена колонок. В `columns`
492
+ указываются только переопределения.
493
+
494
+ ```ts
495
+ const relationalGraph = graph.withRelationalMapping({
496
+ sources: {
497
+ link: {
498
+ table: {
499
+ schema: 'dbo',
500
+ name: 'ControllerAttributeValueLink',
501
+ },
502
+ columns: {
503
+ idOwner: 'owner_id',
504
+ },
505
+ },
506
+ value: {
507
+ table: 'ControllerObjectValue',
508
+ },
509
+ },
510
+ })
511
+ ```
512
+
513
+ Операция передается в Rust одним вызовом. Компилятор выбирает необходимые пути
514
+ связей, формирует синтаксис SQL Server и возвращает описания параметров.
515
+
516
+ ```ts
517
+ const operation = {
518
+ select: ['value.value'],
519
+ parameters: {
520
+ idOwner: 42,
521
+ },
522
+ offset: 0,
523
+ limit: 25,
524
+ }
525
+
526
+ const statement = relationalGraph.compileSqlServer(operation)
527
+
528
+ console.log(statement.sql)
529
+ console.table(statement.bindings)
530
+ console.table(statement.columns)
531
+ console.table(statement.relations)
532
+ ```
533
+
534
+ `statement.bindings` связывает сгенерированные имена, например `p0`, с
535
+ логическими параметрами, например `idOwner`, и содержит их scalar type.
536
+ Потребитель берет значения из `operation.parameters` и передает их
537
+ своему драйверу базы данных.
538
+
539
+ Wire-контракт значений параметров:
540
+
541
+ | Scalar type | Значение в `operation.parameters` |
542
+ | ----------- | -------------------------------------------------------------- |
543
+ | `boolean` | `boolean` |
544
+ | `int32` | целое JavaScript number в диапазоне `int32` |
545
+ | `int64` | безопасное целое JavaScript number или десятичная строка `i64` |
546
+ | `float64` | JSON number |
547
+ | `decimal` | JSON number или десятичная строка без экспоненты |
548
+ | `string` | string |
549
+ | `date` | непрозрачная для compiler строка |
550
+ | `dateTime` | непрозрачная для compiler строка |
551
+ | `binary` | непрозрачная для compiler строка |
552
+ | `json` | любое JSON-совместимое значение |
553
+
554
+ Optional parameter означает отсутствие ключа, а не `null`. Формат строк
555
+ `date`, `dateTime` и `binary`, а также преобразование scalar type в тип конкретного
556
+ DB driver определяет adapter исполнения. SQL compiler не копирует значения в
557
+ `statement`: binding metadata ссылается на исходный ключ параметра.
558
+
559
+ Целевую версию можно указать отдельно от operation:
560
+
561
+ ```ts
562
+ const statement = relationalGraph.compileSqlServer(operation, { version: '2019' })
563
+ ```
564
+
565
+ По умолчанию используется SQL Server 2012. SQL Server 2008 поддерживается для
566
+ запросов без `OFFSET/FETCH`; несовместимая pagination отклоняется до исполнения.
567
+
568
+ Компилятор намеренно не использует логические имена графа как SQL-алиасы.
569
+ Источники получают короткие физические алиасы `t0`, `t1`, а выходные колонки -
570
+ `c0`, `c1`. Поэтому длинные имена источников и пути проекции не зависят от
571
+ ограничений конкретной СУБД на длину идентификаторов.
572
+
573
+ `statement.columns` сопоставляет физические имена колонок с логическими путями
574
+ проекции, содержит их `scalarType`, `nullable` и цепочки relations.
575
+ `statement.relations` описывает выбранный планировщиком путь связей, включая
576
+ направление, cardinality и фактическую обязательность JOIN. Эти metadata позволяют
577
+ потребителю сопоставить плоские SQL-колонки с логическими путями, не разбирая SQL.
578
+ Группировка строк и гидратация коллекций не входят в контракт пакета и остаются
579
+ ответственностью потребителя.
580
+
581
+ ## Oracle
582
+
583
+ Oracle использует тот же planner и operation. Отличается только финальный
584
+ dialect renderer:
585
+
586
+ ```ts
587
+ const statement = relationalGraph.compileOracle(operation)
588
+ const oracle19cStatement = relationalGraph.compileOracle(operation, { version: '19c' })
589
+
590
+ console.log(statement.sql)
591
+ console.table(statement.bindings)
592
+ ```
593
+
594
+ Компилятор генерирует именованные параметры `:p0`, Oracle-кавычки идентификаторов,
595
+ нативный порядок `NULLS FIRST`/`NULLS LAST`, конкатенацию через `||` и пагинацию
596
+ `OFFSET`/`FETCH`, доступную начиная с Oracle 12c. `catalog` в `TableName` относится
597
+ к SQL Server и отклоняется Oracle-компилятором; имя схемы поддерживается.
598
+
599
+ По умолчанию выбирается Oracle 12c. Oracle 11g поддерживается для запросов без
600
+ pagination; `OFFSET/FETCH` для него отклоняется как неподдерживаемая capability.
601
+
602
+ Оба компилятора поддерживают выбор полей проекции, scalar/list параметры,
603
+ ограничения определения,
604
+ условные ограничения, сортировку по умолчанию, соединения
605
+ `INNER JOIN`/`LEFT JOIN`, summary dimensions/measures и пагинацию
606
+ `OFFSET`/`FETCH`. Общий SQL pipeline выбирает пути связей, фазы `WHERE`/`HAVING`
607
+ и обходит expression AST, а dialect renderer отвечает только за синтаксис
608
+ конкретной СУБД.
609
+
610
+ Для обычного record-графа пагинация отклоняется, если план проходит через
611
+ relation с cardinality `many`: JOIN в таком случае меняет количество корневых
612
+ строк. Summary-граф может пагинировать результат после группировки, поэтому
613
+ relation `many` для него допустим. Для пагинации вложенных record-коллекций
614
+ по-прежнему понадобится отдельный split-query plan.
615
+
616
+ Фильтры времени выполнения и пользовательские отображения семантических функций
617
+ намеренно оставлены для последующих этапов.
618
+
619
+ Полный пример графа, отображения, операции, сгенерированного SQL и замера
620
+ производительности компиляции находится в `benchmark/bench.ts`.