@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 +21 -0
- package/README.md +620 -0
- package/definition.d.ts +87 -0
- package/definition.js +112 -0
- package/dsl.d.ts +687 -0
- package/dsl.js +742 -0
- package/index.d.ts +60 -0
- package/index.js +562 -0
- package/package.json +143 -0
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`.
|