@query-graph/core 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -969
- package/definition.d.ts +11 -0
- package/definition.js +1 -2
- package/docs/authoring.md +161 -0
- package/docs/composition.md +100 -0
- package/docs/diagnostics.md +52 -0
- package/docs/sql.md +68 -0
- package/docs/type-system.md +86 -0
- package/dsl.d.ts +182 -1839
- package/dsl.js +57 -763
- package/index.d.ts +8 -100
- package/index.js +13 -568
- package/lib/authoring/definition.js +425 -0
- package/lib/authoring/primitives.js +433 -0
- package/lib/authoring/symbols.js +4 -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 +667 -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,42 +3,38 @@
|
|
|
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
|
+
- summary-графы с dimensions, measures, <code>GROUP BY</code> и <code>HAVING</code>;
|
|
21
|
+
- batch relations для загрузки коллекций после пагинации root;
|
|
22
|
+
- один семантический definition для SQL Server и Oracle.
|
|
24
23
|
|
|
25
24
|
Требуется Node.js 20 или новее.
|
|
26
25
|
|
|
27
|
-
##
|
|
28
|
-
|
|
29
|
-
### Установка
|
|
26
|
+
## Установка
|
|
30
27
|
|
|
31
28
|
```bash
|
|
32
29
|
npm install @query-graph/core
|
|
33
30
|
```
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
`@query-graph/core/dsl`.
|
|
32
|
+
Используйте два entrypoint:
|
|
37
33
|
|
|
38
|
-
|
|
34
|
+
- <code>@query-graph/core/dsl</code> для описания графа и TypeScript типов;
|
|
35
|
+
- <code>@query-graph/core</code> для регистрации и нативной компиляции.
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
синтаксиса конкретной СУБД.
|
|
37
|
+
## Быстрый старт
|
|
42
38
|
|
|
43
39
|
```ts
|
|
44
40
|
import { registerDefinition } from '@query-graph/core'
|
|
@@ -68,14 +64,7 @@ const usersDefinition = defineGraph({
|
|
|
68
64
|
project({ path: 'email', expression: users.field('email'), default: true }),
|
|
69
65
|
],
|
|
70
66
|
})
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 2. Добавьте реляционное отображение
|
|
74
|
-
|
|
75
|
-
Mapping связывает логический source с физической таблицей. Его можно заменить,
|
|
76
|
-
не меняя definition.
|
|
77
67
|
|
|
78
|
-
```ts
|
|
79
68
|
const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
|
|
80
69
|
sources: {
|
|
81
70
|
users: {
|
|
@@ -86,972 +75,109 @@ const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
|
|
|
86
75
|
},
|
|
87
76
|
},
|
|
88
77
|
})
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Mapping переиспользуемых частей можно держать рядом с ними и объединять при
|
|
92
|
-
регистрации итогового графа:
|
|
93
78
|
|
|
94
|
-
```ts
|
|
95
|
-
const usersGraph = registerDefinition(usersDefinition).withRelationalMappings([usersMapping, profileMapping])
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Фрагменты могут дополнять `columns` одного source, если указывают одинаковую
|
|
99
|
-
таблицу. Повтор одинакового отображения безопасен. Разные таблицы для одного
|
|
100
|
-
source или разные физические колонки для одного логического поля возвращают
|
|
101
|
-
структурированную ошибку фазы `mapping`. После объединения mapping всё равно
|
|
102
|
-
должен полностью покрывать sources итогового definition.
|
|
103
|
-
|
|
104
|
-
`registerDefinition` валидирует и индексирует definition в Rust. Обычно граф
|
|
105
|
-
регистрируется один раз при запуске приложения и затем переиспользуется.
|
|
106
|
-
|
|
107
|
-
### 3. Скомпилируйте операцию
|
|
108
|
-
|
|
109
|
-
Operation содержит только изменяемую часть запроса. TypeScript проверяет имена
|
|
110
|
-
полей и параметров относительно конкретного definition.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
79
|
const operation = {
|
|
114
80
|
select: ['id', 'email'],
|
|
115
|
-
parameters: {
|
|
116
|
-
organisationId: 42,
|
|
117
|
-
},
|
|
81
|
+
parameters: { organisationId: 42 },
|
|
118
82
|
} as const satisfies QueryOperation<typeof usersDefinition>
|
|
119
83
|
|
|
120
|
-
const statement = usersGraph.compileSqlServer(operation)
|
|
121
|
-
|
|
122
|
-
console.log(statement.sql)
|
|
123
|
-
console.table(statement.bindings)
|
|
124
|
-
console.table(statement.columns)
|
|
125
|
-
|
|
84
|
+
const statement = usersGraph.compileSqlServer(operation, { version: '2022' })
|
|
126
85
|
type UserRow = ResultOf<typeof usersDefinition, typeof operation>
|
|
127
|
-
// { id: number | string; email: string }
|
|
128
86
|
```
|
|
129
87
|
|
|
130
|
-
|
|
88
|
+
<code>statement</code> содержит:
|
|
131
89
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
```
|
|
90
|
+
| Поле | Назначение |
|
|
91
|
+
| ---------------------- | --------------------------------------------------------------------------------- |
|
|
92
|
+
| <code>sql</code> | параметризованный SQL с placeholders <code>p0</code>, <code>p1</code> и так далее |
|
|
93
|
+
| <code>bindings</code> | связь placeholder с operation parameter и типом |
|
|
94
|
+
| <code>columns</code> | связь SQL alias <code>cN</code> с логическим projection path |
|
|
95
|
+
| <code>objects</code> | presence metadata для nullable вложенных объектов |
|
|
96
|
+
| <code>relations</code> | связи, выбранные planner для этой operation |
|
|
140
97
|
|
|
141
|
-
|
|
142
|
-
|
|
98
|
+
Executor берет значения из <code>operation.parameters</code> по
|
|
99
|
+
<code>bindings</code>, вызывает DB driver и восстанавливает объект по
|
|
100
|
+
<code>columns</code> и <code>objects</code>. Разбирать SQL не нужно.
|
|
143
101
|
|
|
144
|
-
Тот же graph можно
|
|
102
|
+
Тот же mapped graph можно скомпилировать в Oracle:
|
|
145
103
|
|
|
146
104
|
```ts
|
|
147
105
|
const oracleStatement = usersGraph.compileOracle(operation, { version: '19c' })
|
|
148
106
|
```
|
|
149
107
|
|
|
150
|
-
|
|
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
|
-
Обычный жизненный цикл выглядит так:
|
|
108
|
+
## Модель
|
|
192
109
|
|
|
193
110
|
```text
|
|
194
|
-
defineGraph
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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)
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
### Композиция через GraphModule
|
|
314
|
-
|
|
315
|
-
`GraphModule` группирует переиспользуемую часть определения: источники,
|
|
316
|
-
параметры, связи, ограничения, проекцию и сортировку. У модуля нет собственного
|
|
317
|
-
корневого источника, и его нельзя зарегистрировать или скомпилировать отдельно.
|
|
318
|
-
|
|
319
|
-
`defineGraph` объединяет вложенные модули и локальные элементы в один плоский
|
|
320
|
-
`GraphDefinition`. Повторно подключенный объект дедуплицируется по identity. Для
|
|
321
|
-
именованных элементов разные определения с одним ключом приводят к ошибке
|
|
322
|
-
композиции.
|
|
323
|
-
|
|
324
|
-
Constraints не имеют имени: повторно использованный constraint добавляется один
|
|
325
|
-
раз, а разные constraint-объекты сохраняются и объединяются через `AND`. Сведения
|
|
326
|
-
о модулях не передаются в Rust и не входят в wire-формат.
|
|
327
|
-
|
|
328
|
-
Вспомогательные функции API описания графа формируют версионируемое и
|
|
329
|
-
сериализуемое промежуточное представление `GraphDefinition`. Дискриминаторы
|
|
330
|
-
выражений, например `kind: 'eq'`, существуют только в сгенерированном
|
|
331
|
-
представлении. В прикладном коде графа их писать не требуется. TypeScript
|
|
332
|
-
проверяет ссылки на поля источников.
|
|
333
|
-
|
|
334
|
-
Relation path проекции указывать не требуется. При регистрации definition Rust
|
|
335
|
-
находит самый глубокий source, использованный expression, и один раз сохраняет
|
|
336
|
-
путь от root до этого source. Expression может обращаться к root и любым sources
|
|
337
|
-
на одной ветке. Обращение к двум независимым веткам отклоняется как неоднозначная
|
|
338
|
-
проекция. Planner использует уже вычисленные пути и не повторяет inference при
|
|
339
|
-
каждой компиляции operation.
|
|
340
|
-
|
|
341
|
-
### Варианты DSL
|
|
342
|
-
|
|
343
|
-
Canonical `GraphDefinition` не зависит от authoring DSL. Основной authoring API
|
|
344
|
-
доступен через `@query-graph/core/dsl` и использует объектные конфигурации. Позиционный
|
|
345
|
-
`@query-graph/core/definition` сохранён как адаптер совместимости для существующего кода;
|
|
346
|
-
новые графы на него ориентировать не следует.
|
|
347
|
-
|
|
348
|
-
Отдельный пакет `@query-graph/dsl-object` является тонким фасадом над тем же API.
|
|
349
|
-
Он полезен, когда DSL нужен как отдельная зависимость:
|
|
350
|
-
|
|
351
|
-
```bash
|
|
352
|
-
npm install @query-graph/core @query-graph/dsl-object
|
|
353
|
-
```
|
|
111
|
+
defineGraph
|
|
112
|
+
|
|
|
113
|
+
v
|
|
114
|
+
registerDefinition Rust validation + reusable analysis
|
|
115
|
+
|
|
|
116
|
+
v
|
|
117
|
+
withRelationalMapping logical sources -> physical tables
|
|
118
|
+
|
|
|
119
|
+
+---- QueryOperation
|
|
120
|
+
| |
|
|
121
|
+
v v
|
|
122
|
+
compileSqlServer / compileOracle
|
|
123
|
+
|
|
|
124
|
+
v
|
|
125
|
+
CompiledSqlStatement SQL + bindings + result metadata
|
|
126
|
+
|
|
|
127
|
+
v
|
|
128
|
+
application executor DB connection remains outside query-graph
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Понятие | Ответственность |
|
|
132
|
+
| --------------------------------- | ------------------------------------------------------------------ |
|
|
133
|
+
| <code>GraphDefinition</code> | sources, parameters, relations, constraints, projection и ordering |
|
|
134
|
+
| <code>GraphModule</code> | переиспользуемая часть definition без собственного root |
|
|
135
|
+
| <code>QueryGraph</code> | проверенный и проиндексированный нативный граф |
|
|
136
|
+
| <code>RelationalMapping</code> | таблицы, схемы и физические имена колонок |
|
|
137
|
+
| <code>QueryOperation</code> | select, parameters, ordering и pagination одного запроса |
|
|
138
|
+
| <code>CompiledSqlStatement</code> | SQL и контракт результата для executor |
|
|
139
|
+
|
|
140
|
+
## Куда дальше
|
|
141
|
+
|
|
142
|
+
- [Authoring DSL](docs/authoring.md): sources, expressions, relations,
|
|
143
|
+
constraints, projections, ordering и summary.
|
|
144
|
+
- [Composition](docs/composition.md): GraphModule, batchQuery, batchRelation и
|
|
145
|
+
двухфазное выполнение.
|
|
146
|
+
- [Type system](docs/type-system.md): scalar types, nullability,
|
|
147
|
+
<code>QueryOperation</code> и <code>ResultOf</code>.
|
|
148
|
+
- [SQL compilation](docs/sql.md): relational mapping, dialect versions,
|
|
149
|
+
bindings и контракт executor.
|
|
150
|
+
- [Diagnostics](docs/diagnostics.md): фазы ошибок, коды и границы runtime
|
|
151
|
+
validation.
|
|
152
|
+
|
|
153
|
+
## Entry Points
|
|
154
|
+
|
|
155
|
+
Канонический API:
|
|
354
156
|
|
|
355
157
|
```ts
|
|
356
158
|
import { registerDefinition } from '@query-graph/core'
|
|
357
|
-
import {
|
|
358
|
-
|
|
359
|
-
const users = source('users', { id: 'int64' })
|
|
360
|
-
const profiles = source('profiles', {
|
|
361
|
-
id: 'int64',
|
|
362
|
-
userId: 'int64',
|
|
363
|
-
displayName: 'string',
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
const definition = defineGraph({
|
|
367
|
-
name: 'users',
|
|
368
|
-
root: users,
|
|
369
|
-
sources: [users, profiles],
|
|
370
|
-
relations: [
|
|
371
|
-
relation({
|
|
372
|
-
name: 'profile',
|
|
373
|
-
from: users,
|
|
374
|
-
to: profiles,
|
|
375
|
-
on: eq(users.field('id'), profiles.field('userId')),
|
|
376
|
-
cardinality: 'one',
|
|
377
|
-
selection: firstBy(asc(profiles.field('id'))),
|
|
378
|
-
}),
|
|
379
|
-
],
|
|
380
|
-
projection: [
|
|
381
|
-
project({ path: 'id', expression: users.field('id'), default: true }),
|
|
382
|
-
project({ path: 'profile.displayName', expression: profiles.field('displayName') }),
|
|
383
|
-
],
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
const graph = registerDefinition(definition)
|
|
159
|
+
import { defineGraph, relation, project } from '@query-graph/core/dsl'
|
|
387
160
|
```
|
|
388
161
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
],
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
relationalGraph.compileOracle({
|
|
525
|
-
ordering: 'nameAsc',
|
|
526
|
-
parameters,
|
|
527
|
-
})
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
Поле `default: true` можно указать только у одного варианта. Если operation не передает
|
|
531
|
-
`ordering`, planner использует default-вариант; без него SQL строится без `ORDER BY`.
|
|
532
|
-
TypeScript выводит union имен из definition и подключенных graph modules. Rust проверяет имя
|
|
533
|
-
повторно и планирует только связи и параметры выбранной сортировки.
|
|
534
|
-
|
|
535
|
-
### To-one selection
|
|
536
|
-
|
|
537
|
-
`cardinality: 'one'` описывает контракт связи. Если физический источник может
|
|
538
|
-
содержать несколько кандидатов, `firstBy` задает семантическое правило выбора
|
|
539
|
-
одной строки:
|
|
540
|
-
|
|
541
|
-
```ts
|
|
542
|
-
const profile = relation({
|
|
543
|
-
name: 'profile',
|
|
544
|
-
from: users,
|
|
545
|
-
to: profiles,
|
|
546
|
-
on: eq(users.field('id'), profiles.field('userId')),
|
|
547
|
-
selection: firstBy(asc(profiles.field('id'))),
|
|
548
|
-
})
|
|
549
|
-
```
|
|
550
|
-
|
|
551
|
-
Поля сортировки могут обращаться только к target source. Список сортировки
|
|
552
|
-
обязателен; последнее поле следует выбирать уникальным, чтобы результат был
|
|
553
|
-
детерминированным. `required: false` означает `0..1`, а `required: true` -
|
|
554
|
-
ровно одну связанную строку.
|
|
555
|
-
|
|
556
|
-
SQL Server renderer использует `OUTER/CROSS APPLY` и `TOP (1)`, Oracle 12c+
|
|
557
|
-
использует `OUTER/CROSS APPLY` и `FETCH FIRST 1 ROW ONLY`. Эти детали не входят
|
|
558
|
-
в definition. Для Oracle 11g `firstBy` возвращает
|
|
559
|
-
`unsupportedDialectFeature`.
|
|
162
|
+
<code>@query-graph/core/definition</code> сохраняет позиционные фабрики только
|
|
163
|
+
для совместимости линии 1.2.x. <code>@query-graph/dsl-object</code> остается
|
|
164
|
+
тонким deprecated facade над <code>@query-graph/core/dsl</code>. Новому коду эти
|
|
165
|
+
entrypoint не нужны.
|
|
560
166
|
|
|
561
|
-
|
|
167
|
+
Batch-фабрики временно доступны из package root, но новый код должен импортировать
|
|
168
|
+
их из <code>@query-graph/core/dsl</code>.
|
|
562
169
|
|
|
563
|
-
|
|
564
|
-
SQL-конструкции `GROUP BY` и `HAVING`:
|
|
170
|
+
## Архитектура
|
|
565
171
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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,
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
const definition = defineSummaryGraph({
|
|
594
|
-
name: 'teamSummary',
|
|
595
|
-
root: teams,
|
|
596
|
-
sources: [teams, memberships],
|
|
597
|
-
parameters: [workspaceId, minimumMembers],
|
|
598
|
-
relations: [
|
|
599
|
-
relation({
|
|
600
|
-
name: 'members',
|
|
601
|
-
from: teams,
|
|
602
|
-
to: memberships,
|
|
603
|
-
on: eq(teams.field('id'), memberships.field('teamId')),
|
|
604
|
-
cardinality: 'many',
|
|
605
|
-
}),
|
|
606
|
-
],
|
|
607
|
-
constraints: [
|
|
608
|
-
constraint({
|
|
609
|
-
predicate: eq(teams.field('workspaceId'), param(workspaceId)),
|
|
610
|
-
}),
|
|
611
|
-
constraint({
|
|
612
|
-
predicate: gte(memberCount, param(minimumMembers)),
|
|
613
|
-
}),
|
|
614
|
-
],
|
|
615
|
-
dimensions: [teamId],
|
|
616
|
-
measures: [memberCount],
|
|
617
|
-
orderings: [
|
|
618
|
-
ordering({
|
|
619
|
-
name: 'memberCountDesc',
|
|
620
|
-
by: [desc(memberCount)],
|
|
621
|
-
default: true,
|
|
622
|
-
}),
|
|
623
|
-
],
|
|
624
|
-
})
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
Planner включает все объявленные dimensions в идентичность результата и
|
|
628
|
-
компилирует их в `GROUP BY`, даже если operation не возвращает часть из них.
|
|
629
|
-
Constraint без агрегата фильтрует исходное множество через `WHERE`. Constraint
|
|
630
|
-
с агрегатом фильтрует уже построенные группы через `HAVING`. Поэтому автор графа
|
|
631
|
-
не выбирает SQL-фазу вручную и не может случайно поместить агрегат в `WHERE`.
|
|
632
|
-
|
|
633
|
-
Поддерживаются `count()`, `count(expression)`, `countDistinct`, `sum`,
|
|
634
|
-
`average`, `minimum` и `maximum`. Measure можно передавать в сравнения и
|
|
635
|
-
сортировку как expression; DSL разворачивает ссылку в сериализуемое
|
|
636
|
-
семантическое выражение до передачи definition в Rust.
|
|
637
|
-
|
|
638
|
-
Rust отклоняет агрегаты в обычных graph projections, relation predicates и
|
|
639
|
-
dimensions, а также вложенные агрегаты и поля вне объявленных dimensions.
|
|
640
|
-
`count` и `countDistinct` возвращают non-null `int64`; остальные меры nullable,
|
|
641
|
-
поскольку пустое множество или множество из `NULL` не имеет значения агрегата.
|
|
642
|
-
Для сохранения контракта `int64` SQL Server compiler использует `COUNT_BIG`, а
|
|
643
|
-
Oracle compiler - `COUNT`. Dimension должен иметь семантику равенства, поэтому
|
|
644
|
-
`json` не может задавать идентичность группы.
|
|
645
|
-
|
|
646
|
-
Глобальный `DISTINCT` намеренно не выводится из структуры связей. Для количества
|
|
647
|
-
уникальных значений используется `countDistinct`, а summary только из dimensions
|
|
648
|
-
задает уникальные комбинации через группировку. Произвольный SQL subquery также
|
|
649
|
-
не входит в DSL: `exists`, `firstBy` и агрегаты остаются семантическими
|
|
650
|
-
операциями, для которых форму подзапроса при необходимости выбирает compiler.
|
|
651
|
-
|
|
652
|
-
Summary-план с несколькими независимыми relations `many` отклоняется: обычные
|
|
653
|
-
JOIN создали бы декартово размножение веток и могли исказить measures. Ограничение
|
|
654
|
-
соседней ветки следует выражать через `exists`. Независимые меры нескольких
|
|
655
|
-
коллекций потребуют отдельной aggregate-subquery strategy в planner; compiler не
|
|
656
|
-
маскирует эту ситуацию через `DISTINCT`.
|
|
657
|
-
|
|
658
|
-
Полученное определение не содержит SQL, имен таблиц, значений конкретного
|
|
659
|
-
драйвера, объектов построителя или исполняемых функций обратного вызова
|
|
660
|
-
JavaScript. DSL не выполняет планирование запросов: Rust остается единственным
|
|
661
|
-
источником правил валидации и компиляции.
|
|
662
|
-
|
|
663
|
-
## Система типов
|
|
664
|
-
|
|
665
|
-
При `registerDefinition` Rust выводит тип каждого expression из описаний полей,
|
|
666
|
-
параметров и литералов. Проверка выполняется один раз после структурной
|
|
667
|
-
валидации и до построения переиспользуемого `QueryGraph`.
|
|
668
|
-
|
|
669
|
-
Система типов проверяет:
|
|
670
|
-
|
|
671
|
-
- совместимость операндов сравнений;
|
|
672
|
-
- строковые аргументы `like`, `lower`, `upper` и `concat`;
|
|
673
|
-
- общий тип аргументов `coalesce`;
|
|
674
|
-
- boolean-тип условий relations и constraints;
|
|
675
|
-
- допустимость сортировки выражений;
|
|
676
|
-
- конкретный scalar type и nullability каждого поля проекции;
|
|
677
|
-
- типы аргументов агрегатов и их результирующую nullability;
|
|
678
|
-
- соответствие неагрегированных выражений объявленным dimensions.
|
|
679
|
-
|
|
680
|
-
Поддерживаемые семантические функции задаются DSL-функциями `lower`, `upper`,
|
|
681
|
-
`coalesce` и `concat`. Произвольное имя функции не является частью публичного
|
|
682
|
-
DSL: сначала функция должна получить общую семантику типов и реализацию для
|
|
683
|
-
каждого SQL dialect.
|
|
684
|
-
|
|
685
|
-
TypeScript проверяет имена источников, полей и семантических функций во время
|
|
686
|
-
написания definition. Rust остается авторитетной проверкой scalar-типов после
|
|
687
|
-
композиции модулей; Node.js не обходит граф и не дублирует правила вывода типов.
|
|
688
|
-
|
|
689
|
-
`defineGraph` также сохраняет типы параметров и projection paths, включая
|
|
690
|
-
содержимое вложенных `GraphModule`. `registerDefinition` переносит эти сведения
|
|
691
|
-
на native handle, поэтому операция проверяется непосредственно в месте вызова:
|
|
692
|
-
|
|
693
|
-
```ts
|
|
694
|
-
const relationalGraph = registerDefinition(definition).withRelationalMapping(mapping)
|
|
695
|
-
|
|
696
|
-
relationalGraph.compileSqlServer({
|
|
697
|
-
select: ['id', 'profile.displayName'],
|
|
698
|
-
parameters: {
|
|
699
|
-
userId: 42,
|
|
700
|
-
},
|
|
701
|
-
})
|
|
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
|
-
|
|
752
|
-
type DriverScalars = Omit<DefaultScalarOutputTypeMap, 'int64' | 'dateTime'> & {
|
|
753
|
-
int64: bigint
|
|
754
|
-
dateTime: Date
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
type DriverUserRow = ResultOf<typeof definition, typeof operation, DriverScalars>
|
|
758
|
-
```
|
|
759
|
-
|
|
760
|
-
Для точного учета явного `select` operation должна сохранять literal tuple через
|
|
761
|
-
`as const` или `satisfies`. Это только TypeScript API: wire definition, N-API и
|
|
762
|
-
Rust planner не получают дополнительного runtime state.
|
|
763
|
-
|
|
764
|
-
`ResultOf` описывает логическую форму одной строки, восстановленную из путей
|
|
765
|
-
проекции. Он не выполняет запрос и не превращает relation `many` в массив.
|
|
766
|
-
Группировка строк, дедупликация и гидратация коллекций остаются ответственностью
|
|
767
|
-
потребителя модуля.
|
|
768
|
-
|
|
769
|
-
## Диагностика
|
|
770
|
-
|
|
771
|
-
Ошибки N-API имеют имя `QueryGraphError` и сохраняют обычный `message` и stack.
|
|
772
|
-
Для программной обработки доступны:
|
|
773
|
-
|
|
774
|
-
- `code` — категория ошибки на границе пакета;
|
|
775
|
-
- `phase` — `definition`, `mapping`, `operation` или `sql`;
|
|
776
|
-
- `issues` — массив `{ code, location, message }`.
|
|
777
|
-
|
|
778
|
-
```ts
|
|
779
|
-
import type { QueryGraphError } from '@query-graph/core/definition'
|
|
780
|
-
|
|
781
|
-
try {
|
|
782
|
-
relationalGraph.compileSqlServer(operation)
|
|
783
|
-
} catch (cause) {
|
|
784
|
-
const error = cause as QueryGraphError
|
|
785
|
-
|
|
786
|
-
if (error.name === 'QueryGraphError') {
|
|
787
|
-
console.table(error.issues)
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
Wire errors, semantic validation и SQL compilation имеют разные верхнеуровневые
|
|
793
|
-
`code`. Коды отдельных `issues` стабильны и не требуют разбора текста `message`.
|
|
794
|
-
|
|
795
|
-
## Presence объектной проекции
|
|
796
|
-
|
|
797
|
-
Nullable-поля не позволяют определить, отсутствует ли вложенный объект: существующий
|
|
798
|
-
объект может корректно содержать только `NULL`. Для такой границы definition явно
|
|
799
|
-
задаёт выражение присутствия через `projectObject`:
|
|
800
|
-
|
|
801
|
-
```ts
|
|
802
|
-
const definition = defineGraph({
|
|
803
|
-
name: 'users',
|
|
804
|
-
root: users,
|
|
805
|
-
sources: [users, profiles],
|
|
806
|
-
relations: [
|
|
807
|
-
relation({
|
|
808
|
-
name: 'profile',
|
|
809
|
-
from: users,
|
|
810
|
-
to: profiles,
|
|
811
|
-
on: eq(users.field('id'), profiles.field('userId')),
|
|
812
|
-
}),
|
|
813
|
-
],
|
|
814
|
-
objects: [
|
|
815
|
-
projectObject({
|
|
816
|
-
path: 'profile',
|
|
817
|
-
presence: profiles.field('id'),
|
|
818
|
-
}),
|
|
819
|
-
],
|
|
820
|
-
projection: [
|
|
821
|
-
project({
|
|
822
|
-
path: 'profile.displayName',
|
|
823
|
-
expression: profiles.field('displayName'),
|
|
824
|
-
}),
|
|
825
|
-
],
|
|
826
|
-
})
|
|
827
|
-
```
|
|
828
|
-
|
|
829
|
-
`presence` должно возвращать не-`NULL`, когда объект существует, и `NULL`, когда
|
|
830
|
-
его нет. Обычно для этого используется обязательный идентификатор source за
|
|
831
|
-
optional relation. Маркер не является доступным для выбора полем проекции.
|
|
832
|
-
|
|
833
|
-
Если operation выбирает хотя бы одно поле под `profile`, planner включает
|
|
834
|
-
presence-выражение в план. SQL compiler добавляет скрытую колонку, например `o0`,
|
|
835
|
-
и возвращает отдельные metadata:
|
|
836
|
-
|
|
837
|
-
```ts
|
|
838
|
-
statement.objects
|
|
839
|
-
// [{ path: 'profile', presenceColumn: 'o0' }]
|
|
840
|
-
```
|
|
841
|
-
|
|
842
|
-
`statement.columns` по-прежнему содержит только публичные поля `c0`, `c1` и так
|
|
843
|
-
далее. Исполнитель сначала восстанавливает результат по `statement.columns`, а
|
|
844
|
-
затем устанавливает объект по `path` в `null`, если соответствующая исходная
|
|
845
|
-
колонка `presenceColumn` равна `NULL`. Для вложенных объектов metadata упорядочены
|
|
846
|
-
от самого глубокого path к внешнему.
|
|
847
|
-
|
|
848
|
-
Таким образом, исполнитель не угадывает отсутствие объекта по правилу «все поля
|
|
849
|
-
равны `NULL`». `ResultOf` также учитывает declaration и выводит
|
|
850
|
-
`profile: { ... } | null`. Пакет только компилирует SQL и metadata; выполнение
|
|
851
|
-
запроса и materialization остаются у потребителя.
|
|
852
|
-
|
|
853
|
-
## SQL Server
|
|
854
|
-
|
|
855
|
-
Реляционное отображение связывает логические источники с физическими таблицами.
|
|
856
|
-
По умолчанию логические имена полей используются как имена колонок. В `columns`
|
|
857
|
-
указываются только переопределения.
|
|
858
|
-
|
|
859
|
-
```ts
|
|
860
|
-
const relationalGraph = graph.withRelationalMapping({
|
|
861
|
-
sources: {
|
|
862
|
-
users: {
|
|
863
|
-
table: {
|
|
864
|
-
schema: 'dbo',
|
|
865
|
-
name: 'users',
|
|
866
|
-
},
|
|
867
|
-
},
|
|
868
|
-
profiles: {
|
|
869
|
-
table: {
|
|
870
|
-
schema: 'dbo',
|
|
871
|
-
name: 'user_profiles',
|
|
872
|
-
},
|
|
873
|
-
columns: {
|
|
874
|
-
userId: 'user_id',
|
|
875
|
-
displayName: 'display_name',
|
|
876
|
-
},
|
|
877
|
-
},
|
|
878
|
-
},
|
|
879
|
-
})
|
|
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
|
-
|
|
906
|
-
`statement.bindings` связывает сгенерированные имена, например `p0`, с
|
|
907
|
-
логическими параметрами, например `userId`, и содержит их scalar type.
|
|
908
|
-
Потребитель берет значения из `operation.parameters` и передает их
|
|
909
|
-
своему драйверу базы данных.
|
|
910
|
-
|
|
911
|
-
Wire-контракт значений параметров:
|
|
912
|
-
|
|
913
|
-
| Scalar type | Значение в `operation.parameters` |
|
|
914
|
-
| ----------- | -------------------------------------------------------------- |
|
|
915
|
-
| `boolean` | `boolean` |
|
|
916
|
-
| `int32` | целое JavaScript number в диапазоне `int32` |
|
|
917
|
-
| `int64` | безопасное целое JavaScript number или десятичная строка `i64` |
|
|
918
|
-
| `float64` | JSON number |
|
|
919
|
-
| `decimal` | JSON number или десятичная строка без экспоненты |
|
|
920
|
-
| `string` | string |
|
|
921
|
-
| `date` | непрозрачная для compiler строка |
|
|
922
|
-
| `dateTime` | непрозрачная для compiler строка |
|
|
923
|
-
| `binary` | непрозрачная для compiler строка |
|
|
924
|
-
| `json` | любое JSON-совместимое значение |
|
|
925
|
-
|
|
926
|
-
Optional parameter означает отсутствие ключа, а не `null`. Формат строк
|
|
927
|
-
`date`, `dateTime` и `binary`, а также преобразование scalar type в тип конкретного
|
|
928
|
-
DB driver определяет adapter исполнения. SQL compiler не копирует значения в
|
|
929
|
-
`statement`: binding metadata ссылается на исходный ключ параметра.
|
|
930
|
-
|
|
931
|
-
Целевую версию можно указать отдельно от operation:
|
|
932
|
-
|
|
933
|
-
```ts
|
|
934
|
-
const statement = relationalGraph.compileSqlServer(operation, { version: '2019' })
|
|
935
|
-
```
|
|
936
|
-
|
|
937
|
-
По умолчанию используется SQL Server 2012. SQL Server 2008 поддерживается для
|
|
938
|
-
запросов без `OFFSET/FETCH`; несовместимая pagination отклоняется до исполнения.
|
|
939
|
-
|
|
940
|
-
Компилятор намеренно не использует логические имена графа как SQL-алиасы.
|
|
941
|
-
Источники получают короткие физические алиасы `t0`, `t1`, а выходные колонки -
|
|
942
|
-
`c0`, `c1`. Поэтому длинные имена источников и пути проекции не зависят от
|
|
943
|
-
ограничений конкретной СУБД на длину идентификаторов.
|
|
944
|
-
|
|
945
|
-
`statement.columns` сопоставляет физические имена колонок с логическими путями
|
|
946
|
-
проекции, содержит их `scalarType`, `nullable` и цепочки relations.
|
|
947
|
-
`statement.relations` описывает выбранный планировщиком путь связей, включая
|
|
948
|
-
направление, cardinality и фактическую обязательность JOIN. Эти metadata позволяют
|
|
949
|
-
потребителю сопоставить плоские SQL-колонки с логическими путями, не разбирая SQL.
|
|
950
|
-
Группировка строк и гидратация коллекций не входят в контракт пакета и остаются
|
|
951
|
-
ответственностью потребителя.
|
|
952
|
-
|
|
953
|
-
## Oracle
|
|
954
|
-
|
|
955
|
-
Oracle использует тот же planner и operation. Отличается только финальный
|
|
956
|
-
dialect renderer:
|
|
957
|
-
|
|
958
|
-
```ts
|
|
959
|
-
const statement = relationalGraph.compileOracle(operation)
|
|
960
|
-
const oracle19cStatement = relationalGraph.compileOracle(operation, { version: '19c' })
|
|
961
|
-
|
|
962
|
-
console.log(statement.sql)
|
|
963
|
-
console.table(statement.bindings)
|
|
964
|
-
```
|
|
965
|
-
|
|
966
|
-
Компилятор генерирует именованные параметры `:p0`, Oracle-кавычки идентификаторов,
|
|
967
|
-
нативный порядок `NULLS FIRST`/`NULLS LAST`, конкатенацию через `||` и пагинацию
|
|
968
|
-
`OFFSET`/`FETCH`, доступную начиная с Oracle 12c. `catalog` в `TableName` относится
|
|
969
|
-
к SQL Server и отклоняется Oracle-компилятором; имя схемы поддерживается.
|
|
970
|
-
|
|
971
|
-
По умолчанию выбирается Oracle 12c. Oracle 11g поддерживается для запросов без
|
|
972
|
-
pagination; `OFFSET/FETCH` для него отклоняется как неподдерживаемая capability.
|
|
973
|
-
|
|
974
|
-
Оба компилятора поддерживают выбор полей проекции, scalar/list параметры,
|
|
975
|
-
ограничения определения,
|
|
976
|
-
условные ограничения, сортировку по умолчанию, соединения
|
|
977
|
-
`INNER JOIN`/`LEFT JOIN`, summary dimensions/measures и пагинацию
|
|
978
|
-
`OFFSET`/`FETCH`. Общий SQL pipeline выбирает пути связей, фазы `WHERE`/`HAVING`
|
|
979
|
-
и обходит expression AST, а dialect renderer отвечает только за синтаксис
|
|
980
|
-
конкретной СУБД.
|
|
981
|
-
|
|
982
|
-
Для обычного record-графа пагинация отклоняется, если план проходит через
|
|
983
|
-
relation с cardinality `many`: JOIN в таком случае меняет количество корневых
|
|
984
|
-
строк. Summary-граф может пагинировать результат после группировки, поэтому
|
|
985
|
-
relation `many` для него допустим. Для пагинации вложенных record-коллекций
|
|
986
|
-
по-прежнему понадобится отдельный split-query plan.
|
|
987
|
-
|
|
988
|
-
Фильтры времени выполнения и пользовательские отображения семантических функций
|
|
989
|
-
намеренно оставлены для последующих этапов.
|
|
990
|
-
|
|
991
|
-
Полный пример графа, отображения, операции, сгенерированного SQL и замера
|
|
992
|
-
производительности компиляции находится в `benchmark/bench.ts`.
|
|
993
|
-
|
|
994
|
-
## Batch-связи и двухфазное выполнение
|
|
995
|
-
|
|
996
|
-
Поля из отдельного запроса подключаются без изменения обычного relational-графа:
|
|
997
|
-
|
|
998
|
-
```ts
|
|
999
|
-
const childrenQuery = batchQuery({
|
|
1000
|
-
graph: childrenGraph,
|
|
1001
|
-
key: {
|
|
1002
|
-
path: 'parentId',
|
|
1003
|
-
parameter: parentIds,
|
|
1004
|
-
},
|
|
1005
|
-
})
|
|
1006
|
-
|
|
1007
|
-
const children = batchRelation({
|
|
1008
|
-
name: 'children',
|
|
1009
|
-
from: 'id',
|
|
1010
|
-
query: childrenQuery,
|
|
1011
|
-
cardinality: 'many',
|
|
1012
|
-
})
|
|
1013
|
-
|
|
1014
|
-
const graph = composeGraph({ root: rootGraph, relations: [children] })
|
|
1015
|
-
const plan = graph.compileOraclePlan({
|
|
1016
|
-
select: ['id', 'children.id'],
|
|
1017
|
-
parameters: {},
|
|
1018
|
-
limit: 20,
|
|
1019
|
-
})
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
`batchQuery` принадлежит child-модулю и один раз закрепляет его graph,
|
|
1023
|
-
projection path ключа и list-параметр. Поэтому разные root-графы могут
|
|
1024
|
-
переиспользовать query, не повторяя внутренний контракт child-запроса.
|
|
1025
|
-
|
|
1026
|
-
`composeGraph` оставляет в JavaScript только декларативный фасад. Проверка
|
|
1027
|
-
совместимости ключей и параметров, разделение operation на root и batch-шаги и
|
|
1028
|
-
компиляция SQL выполняются в Rust. `compileOraclePlan` или
|
|
1029
|
-
`compileSqlServerPlan` сразу компилирует только `plan.root`; child SQL создаётся
|
|
1030
|
-
при вызове `compileBatch`. Plan сохраняет выбранные dialect и version, поэтому
|
|
1031
|
-
отложенный statement компилируется с теми же настройками, что и root.
|
|
1032
|
-
|
|
1033
|
-
Для подготовки child bindings каждый шаг в `plan.batches` также содержит имя
|
|
1034
|
-
ключевого параметра `keyParameter` и снимок статических `parameters`. Executor
|
|
1035
|
-
объединяет их с `{ [batch.keyParameter]: keys }` и сопоставляет полученные значения
|
|
1036
|
-
с возвращёнными `statement.bindings`.
|
|
1037
|
-
|
|
1038
|
-
Executor потребителя выполняет `plan.root`, собирает уникальные ненулевые
|
|
1039
|
-
значения `parentKey` из `plan.batches`, вызывает
|
|
1040
|
-
`plan.compileBatch('children', parentIds)`, выполняет child statement и
|
|
1041
|
-
связывает строки по `childKey`. `cardinality: 'one'` означает объект или `null`,
|
|
1042
|
-
а `many` — массив. Query-graph не подключается к БД, не дедуплицирует ключи, не
|
|
1043
|
-
выполняет statements и не гидратирует результат; при пустом наборе ключей
|
|
1044
|
-
executor должен пропустить child-запрос.
|
|
1045
|
-
|
|
1046
|
-
Ordering, offset и limit корневой operation применяются только к `plan.root`.
|
|
1047
|
-
Batch-связь может задать собственный `ordering`, но child pagination не
|
|
1048
|
-
добавляется. Batch-поля выбираются только явно (`children.id`); без prefixed
|
|
1049
|
-
path соответствующего шага в `plan.batches` нет. Флаги injection в metadata
|
|
1050
|
-
показывают executor, какие ключи были добавлены исключительно для связывания и
|
|
1051
|
-
не должны попасть в публичный результат.
|
|
172
|
+
<code>query-graph-core</code> не зависит от Node.js. В нем находятся wire model,
|
|
173
|
+
analysis, type checker, planner, relational mapping и SQL compilers. Корневой
|
|
174
|
+
Rust crate содержит только N-API adapter и перевод diagnostics/metadata в
|
|
175
|
+
JavaScript.
|
|
1052
176
|
|
|
1053
|
-
|
|
177
|
+
SQL execution намеренно остается у потребителя. Это позволяет использовать
|
|
178
|
+
любой pool, transaction model, tracing и политику retry, не связывая planner с
|
|
179
|
+
конкретным DB driver.
|
|
1054
180
|
|
|
1055
181
|
### Rule 34
|
|
1056
182
|
|
|
1057
|
-
|
|
183
|
+
Every sufficiently generic abstraction eventually gets an implementation.
|