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