@query-graph/core 1.1.5 → 1.1.7
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 +104 -20
- package/definition.d.ts +1 -1
- package/definition.js +13 -0
- package/docs/authoring.md +108 -10
- package/docs/composition.md +1 -1
- package/docs/diagnostics.md +25 -1
- package/docs/sql.md +89 -1
- package/docs/type-system.md +63 -1
- package/dsl.d.ts +20 -0
- package/dsl.js +13 -0
- package/lib/authoring/definition.js +1 -1
- package/lib/authoring/primitives.js +91 -3
- package/lib/authoring/symbols.js +2 -0
- package/native.d.ts +1 -0
- package/native.js +50 -50
- package/package.json +5 -5
- package/types/authoring.d.ts +133 -21
- package/types/composition.d.ts +3 -21
- package/types/model.d.ts +25 -3
- package/types/runtime.d.ts +14 -8
package/LICENSE
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Viktor Marichev
|
|
4
|
+
Copyright (c) 2020 N-API for Rust contributors
|
|
4
5
|
|
|
5
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
7
|
of this software and associated documentation files (the "Software"), to deal
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- выбор JOIN только для реально запрошенных полей и условий;
|
|
18
18
|
- scalar и list parameters без подстановки значений в SQL;
|
|
19
19
|
- <code>exists</code>, <code>firstBy</code>, nullable projection objects и именованные ordering;
|
|
20
|
+
- вычисляемые проекции с переносимыми строковыми и числовыми функциями;
|
|
20
21
|
- summary-графы с dimensions, measures, <code>GROUP BY</code> и <code>HAVING</code>;
|
|
21
22
|
- batch relations для загрузки коллекций после пагинации root;
|
|
22
23
|
- один семантический definition для SQL Server и Oracle.
|
|
@@ -38,8 +39,8 @@ npm install @query-graph/core
|
|
|
38
39
|
|
|
39
40
|
```ts
|
|
40
41
|
import { registerDefinition } from '@query-graph/core'
|
|
41
|
-
import { constraint, defineGraph, eq,
|
|
42
|
-
import type {
|
|
42
|
+
import { constraint, defineGraph, eq, project, requiredParam, source } from '@query-graph/core/dsl'
|
|
43
|
+
import type { ResultOf } from '@query-graph/core/dsl'
|
|
43
44
|
|
|
44
45
|
const users = source('users', {
|
|
45
46
|
id: 'int64',
|
|
@@ -47,7 +48,7 @@ const users = source('users', {
|
|
|
47
48
|
email: 'string',
|
|
48
49
|
})
|
|
49
50
|
|
|
50
|
-
const organisationId =
|
|
51
|
+
const organisationId = requiredParam('organisationId', 'int64')
|
|
51
52
|
|
|
52
53
|
const usersDefinition = defineGraph({
|
|
53
54
|
name: 'users',
|
|
@@ -56,7 +57,7 @@ const usersDefinition = defineGraph({
|
|
|
56
57
|
parameters: [organisationId],
|
|
57
58
|
constraints: [
|
|
58
59
|
constraint({
|
|
59
|
-
predicate: eq(users.field('organisationId'),
|
|
60
|
+
predicate: eq(users.field('organisationId'), organisationId),
|
|
60
61
|
}),
|
|
61
62
|
],
|
|
62
63
|
projection: [
|
|
@@ -76,13 +77,8 @@ const usersGraph = registerDefinition(usersDefinition).withRelationalMapping({
|
|
|
76
77
|
},
|
|
77
78
|
})
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
parameters: { organisationId: 42 },
|
|
82
|
-
} as const satisfies QueryOperation<typeof usersDefinition>
|
|
83
|
-
|
|
84
|
-
const statement = usersGraph.compileSqlServer(operation, { version: '2022' })
|
|
85
|
-
type UserRow = ResultOf<typeof usersDefinition, typeof operation>
|
|
80
|
+
const statement = usersGraph.compileSqlServer({ parameters: { organisationId: 42 } }, { version: '2022' })
|
|
81
|
+
type UserRow = ResultOf<typeof usersDefinition>
|
|
86
82
|
```
|
|
87
83
|
|
|
88
84
|
<code>statement</code> содержит:
|
|
@@ -95,16 +91,108 @@ type UserRow = ResultOf<typeof usersDefinition, typeof operation>
|
|
|
95
91
|
| <code>objects</code> | presence metadata для nullable вложенных объектов |
|
|
96
92
|
| <code>relations</code> | связи, выбранные planner для этой operation |
|
|
97
93
|
|
|
98
|
-
Executor берет значения из <code>
|
|
94
|
+
Executor берет значения из <code>parameters</code> переданной operation по
|
|
99
95
|
<code>bindings</code>, вызывает DB driver и восстанавливает объект по
|
|
100
96
|
<code>columns</code> и <code>objects</code>. Разбирать SQL не нужно.
|
|
101
97
|
|
|
102
98
|
Тот же mapped graph можно скомпилировать в Oracle:
|
|
103
99
|
|
|
104
100
|
```ts
|
|
105
|
-
const oracleStatement = usersGraph.compileOracle(
|
|
101
|
+
const oracleStatement = usersGraph.compileOracle({ parameters: { organisationId: 42 } }, { version: '19c' })
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Зачем нужен planner
|
|
105
|
+
|
|
106
|
+
В реальном графе преимущество проявляется, когда одна definition обслуживает
|
|
107
|
+
разные формы результата:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
orders
|
|
111
|
+
|-- customer
|
|
112
|
+
| `-- organisation
|
|
113
|
+
|-- latestPayment [firstBy]
|
|
114
|
+
`-- items [batch many]
|
|
115
|
+
|
|
116
|
+
constraints: organisationId, exists(activePromotion)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
<code>ordersDefinition</code> содержит показанные на схеме relations и
|
|
120
|
+
projection. Его небанальные части выглядят так:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
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'))),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const hasActivePromotion = constraint({
|
|
132
|
+
predicate: exists(promotions, eq(promotions.field('active'), true)),
|
|
133
|
+
})
|
|
134
|
+
|
|
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],
|
|
144
|
+
})
|
|
145
|
+
|
|
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',
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
const mappedOrders = registerDefinition(ordersDefinition).withRelationalMapping(orderMapping)
|
|
157
|
+
const ordersGraph = composeGraph({ root: mappedOrders, relations: [items] })
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Обычные source и projection declarations здесь свернуты в именованные
|
|
161
|
+
константы. <code>customerObject</code> и <code>latestPaymentObject</code> созданы
|
|
162
|
+
через <code>projectObject</code>, поэтому отсутствующая to-one строка становится
|
|
163
|
+
<code>null</code>.
|
|
164
|
+
|
|
165
|
+
Одна и та же definition теперь обслуживает дешевый список:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const listPlan = ordersGraph.compileSqlServerPlan({
|
|
169
|
+
select: ['id'],
|
|
170
|
+
parameters: { organisationId: 42 },
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
listPlan.root.relations // []
|
|
174
|
+
listPlan.batches // []
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
И подробное представление:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const detailsPlan = ordersGraph.compileSqlServerPlan({
|
|
181
|
+
select: ['id', 'customer.name', 'latestPayment.amount', 'items.name'],
|
|
182
|
+
parameters: { organisationId: 42 },
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
detailsPlan.root.relations.map(({ name }) => name)
|
|
186
|
+
// ['customer', 'latestPayment']
|
|
187
|
+
|
|
188
|
+
detailsPlan.batches.map(({ name }) => name)
|
|
189
|
+
// ['items']
|
|
106
190
|
```
|
|
107
191
|
|
|
192
|
+
Planner не добавляет невыбранную <code>customerOrganisation</code>,
|
|
193
|
+
<code>exists(activePromotion)</code> остается semijoin без размножения root-строк,
|
|
194
|
+
а <code>items</code> загружается одним batch-запросом для всех найденных orders.
|
|
195
|
+
|
|
108
196
|
## Модель
|
|
109
197
|
|
|
110
198
|
```text
|
|
@@ -160,9 +248,9 @@ import { defineGraph, relation, project } from '@query-graph/core/dsl'
|
|
|
160
248
|
```
|
|
161
249
|
|
|
162
250
|
<code>@query-graph/core/definition</code> сохраняет позиционные фабрики только
|
|
163
|
-
для совместимости
|
|
164
|
-
|
|
165
|
-
entrypoint не нужны.
|
|
251
|
+
для обратной совместимости с ранним API 1.x.
|
|
252
|
+
<code>@query-graph/dsl-object</code> остается тонким deprecated facade над
|
|
253
|
+
<code>@query-graph/core/dsl</code>. Новому коду эти entrypoint не нужны.
|
|
166
254
|
|
|
167
255
|
Batch-фабрики временно доступны из package root, но новый код должен импортировать
|
|
168
256
|
их из <code>@query-graph/core/dsl</code>.
|
|
@@ -177,7 +265,3 @@ JavaScript.
|
|
|
177
265
|
SQL execution намеренно остается у потребителя. Это позволяет использовать
|
|
178
266
|
любой pool, transaction model, tracing и политику retry, не связывая planner с
|
|
179
267
|
конкретным DB driver.
|
|
180
|
-
|
|
181
|
-
### Rule 34
|
|
182
|
-
|
|
183
|
-
Every sufficiently generic abstraction eventually gets an implementation.
|
package/definition.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ import type {
|
|
|
30
30
|
} from './dsl.js'
|
|
31
31
|
|
|
32
32
|
/** @deprecated Import authoring primitives from `@query-graph/core/dsl`. */
|
|
33
|
-
export const GRAPH_DEFINITION_VERSION:
|
|
33
|
+
export const GRAPH_DEFINITION_VERSION: 12
|
|
34
34
|
|
|
35
35
|
/** @deprecated Use `source.field(name)` from `@query-graph/core/dsl`. */
|
|
36
36
|
export function field<
|
package/definition.js
CHANGED
|
@@ -65,13 +65,22 @@ exports.hidden = objectDsl.hidden
|
|
|
65
65
|
exports.source = objectDsl.source
|
|
66
66
|
exports.field = field
|
|
67
67
|
exports.requiredParameter = objectDsl.requiredParameter
|
|
68
|
+
exports.requiredParam = objectDsl.requiredParam
|
|
68
69
|
exports.optionalParameter = objectDsl.optionalParameter
|
|
70
|
+
exports.optionalParam = objectDsl.optionalParam
|
|
69
71
|
exports.param = objectDsl.param
|
|
70
72
|
exports.literal = objectDsl.literal
|
|
71
73
|
exports.integer = objectDsl.integer
|
|
72
74
|
exports.decimal = objectDsl.decimal
|
|
73
75
|
exports.requiredListParameter = objectDsl.requiredListParameter
|
|
76
|
+
exports.requiredListParam = objectDsl.requiredListParam
|
|
74
77
|
exports.optionalListParameter = objectDsl.optionalListParameter
|
|
78
|
+
exports.optionalListParam = objectDsl.optionalListParam
|
|
79
|
+
exports.requiredGroupsParameter = objectDsl.requiredGroupsParameter
|
|
80
|
+
exports.requiredGroupsParam = objectDsl.requiredGroupsParam
|
|
81
|
+
exports.optionalGroupsParameter = objectDsl.optionalGroupsParameter
|
|
82
|
+
exports.optionalGroupsParam = objectDsl.optionalGroupsParam
|
|
83
|
+
exports.allGroups = objectDsl.allGroups
|
|
75
84
|
exports.eq = objectDsl.eq
|
|
76
85
|
exports.neq = objectDsl.neq
|
|
77
86
|
exports.lt = objectDsl.lt
|
|
@@ -86,11 +95,15 @@ exports.not = objectDsl.not
|
|
|
86
95
|
exports.isNull = objectDsl.isNull
|
|
87
96
|
exports.isNotNull = objectDsl.isNotNull
|
|
88
97
|
exports.inParameter = objectDsl.inParameter
|
|
98
|
+
exports.inParam = objectDsl.inParam
|
|
89
99
|
exports.exists = objectDsl.exists
|
|
90
100
|
exports.lower = objectDsl.lower
|
|
91
101
|
exports.upper = objectDsl.upper
|
|
92
102
|
exports.coalesce = objectDsl.coalesce
|
|
93
103
|
exports.concat = objectDsl.concat
|
|
104
|
+
exports.divide = objectDsl.divide
|
|
105
|
+
exports.truncate = objectDsl.truncate
|
|
106
|
+
exports.modulo = objectDsl.modulo
|
|
94
107
|
exports.count = objectDsl.count
|
|
95
108
|
exports.countDistinct = objectDsl.countDistinct
|
|
96
109
|
exports.sum = objectDsl.sum
|
package/docs/authoring.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Authoring DSL
|
|
2
2
|
|
|
3
3
|
Authoring API находится в <code>@query-graph/core/dsl</code>. Он создает
|
|
4
|
-
семантический definition версии
|
|
4
|
+
семантический definition версии 12, который не содержит таблиц, SQL aliases или
|
|
5
5
|
синтаксиса диалекта.
|
|
6
6
|
|
|
7
7
|
## Sources и поля
|
|
@@ -22,26 +22,124 @@ const users = source('users', {
|
|
|
22
22
|
## Параметры
|
|
23
23
|
|
|
24
24
|
```ts
|
|
25
|
-
const id =
|
|
26
|
-
const status =
|
|
27
|
-
const ids =
|
|
25
|
+
const id = requiredParam('id', 'int64')
|
|
26
|
+
const status = optionalParam('status', 'string')
|
|
27
|
+
const ids = requiredListParam('ids', 'int64')
|
|
28
|
+
const optionalIds = optionalListParam('optionalIds', 'int64')
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
Scalar parameter
|
|
31
|
-
<code>
|
|
31
|
+
Scalar parameter передается в expression напрямую, list parameter используется
|
|
32
|
+
через <code>inParam</code>. Явная форма <code>param(id)</code> поддерживается
|
|
33
|
+
для совместимости. Полные имена <code>requiredParameter</code>,
|
|
34
|
+
<code>optionalParameter</code>, <code>requiredListParameter</code>,
|
|
35
|
+
<code>optionalListParameter</code> и <code>inParameter</code> остаются алиасами
|
|
36
|
+
тех же factories. Optional parameter не активирует constraint с
|
|
32
37
|
<code>when</code>, пока значение отсутствует.
|
|
33
38
|
|
|
39
|
+
### Группированные параметры
|
|
40
|
+
|
|
41
|
+
Grouped list parameter описывает массив групп <code>{ key, values }</code>.
|
|
42
|
+
Каждая группа содержит один ключ и список значений:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { allGroups, and, constraint, eq, inParam, optionalGroupsParam, source } from '@query-graph/core/dsl'
|
|
46
|
+
|
|
47
|
+
const attributes = source('attributes', { name: 'string', valueId: 'int64' })
|
|
48
|
+
const filters = optionalGroupsParam('filters', { key: 'string', value: 'int64' })
|
|
49
|
+
const attributeFilter = constraint({
|
|
50
|
+
predicate: allGroups(filters, ({ key, values }) =>
|
|
51
|
+
and(eq(attributes.field('name'), key), inParam(attributes.field('valueId'), values)),
|
|
52
|
+
),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Operation parameters:
|
|
56
|
+
const parameters = {
|
|
57
|
+
filters: [
|
|
58
|
+
{ key: 'color', values: [1, 2] },
|
|
59
|
+
{ key: 'size', values: [3] },
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Добавьте <code>filters</code> в <code>parameters</code> definition, а
|
|
65
|
+
<code>attributeFilter</code> в <code>constraints</code>. Если разные группы
|
|
66
|
+
должны совпасть с разными связанными строками, поместите <code>exists</code>
|
|
67
|
+
внутрь callback и объявите соответствующую relation.
|
|
68
|
+
|
|
69
|
+
<code>requiredGroupsParam</code> требует присутствия массива в operation.
|
|
70
|
+
Полные имена <code>optionalGroupsParameter</code> и
|
|
71
|
+
<code>requiredGroupsParameter</code> являются алиасами тех же factories.
|
|
72
|
+
Неизвестные поля конфигурации, scalar types и некорректные аргументы factories
|
|
73
|
+
отклоняются сразу при authoring.
|
|
74
|
+
|
|
75
|
+
Callback <code>allGroups</code> вызывается ровно один раз при построении AST,
|
|
76
|
+
а не для каждой runtime-группы. <code>key</code> является типизированным scalar
|
|
77
|
+
expression, <code>values</code> является отдельной scoped list reference только
|
|
78
|
+
для <code>inParam</code>/<code>inParameter</code>. Ни grouped parameter, ни
|
|
79
|
+
<code>values</code> нельзя передать в <code>param</code> или использовать как
|
|
80
|
+
обычное scalar expression. <code>values</code> также нельзя объявить root parameter.
|
|
81
|
+
|
|
82
|
+
Каждый binder получает UUID scope через <code>node:crypto.randomUUID()</code>,
|
|
83
|
+
независимо от загрузки отдельных экземпляров или копий DSL. Вложенный callback
|
|
84
|
+
может захватить <code>key</code> и <code>values</code> внешнего callback;
|
|
85
|
+
одноименные параметры не смешивают их scopes. Ссылки за пределами своего binder,
|
|
86
|
+
в том числе захваченные из соседнего callback, отклоняются Rust при регистрации.
|
|
87
|
+
Scope является внутренним идентификатором, его конкретное значение не гарантируется.
|
|
88
|
+
|
|
89
|
+
Семантика компиляции:
|
|
90
|
+
|
|
91
|
+
- отсутствующий optional parameter и пустой массив групп дают <code>true</code>;
|
|
92
|
+
- неактивное тело не требует optional scalar parameters или JOIN, используемых
|
|
93
|
+
только внутри него;
|
|
94
|
+
- predicates всех групп объединяются через <code>AND</code>;
|
|
95
|
+
- повторяющиеся ключи остаются независимыми группами, их values не объединяются;
|
|
96
|
+
- пустой <code>values</code> дает <code>false</code> в <code>inParam</code>;
|
|
97
|
+
- nullable predicate сохраняет обычную трехзначную SQL-семантику.
|
|
98
|
+
|
|
99
|
+
Rust разрешает <code>allGroups</code> только в constraint predicates, включая
|
|
100
|
+
вложенные boolean expressions и <code>exists</code>. Projection, ordering,
|
|
101
|
+
relation predicates и aggregate bodies не поддерживают binder.
|
|
102
|
+
|
|
103
|
+
Wire definition сохраняет тип элементов в <code>scalarType</code> и добавляет
|
|
104
|
+
<code>shape: 'groupedList'</code> и <code>keyType</code>. AST использует
|
|
105
|
+
<code>{ kind: 'allGroups', parameter, scope, predicate }</code>,
|
|
106
|
+
<code>{ kind: 'groupKey', scope }</code> и
|
|
107
|
+
<code>{ kind: 'inGroupValues', expression, scope }</code>.
|
|
108
|
+
|
|
34
109
|
## Expressions
|
|
35
110
|
|
|
36
111
|
DSL предоставляет сравнения, boolean groups, null tests, semantic functions,
|
|
37
112
|
агрегаты и literals:
|
|
38
113
|
|
|
39
114
|
```ts
|
|
40
|
-
const predicate = and(eq(users.field('organisationId'),
|
|
115
|
+
const predicate = and(eq(users.field('organisationId'), organisationId), isNull(users.field('dateDelete')))
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Semantic functions позволяют оставить вычисляемые поля частью графа, не
|
|
119
|
+
привязывая definition к синтаксису Oracle или SQL Server:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const durationProjection = [
|
|
123
|
+
project({
|
|
124
|
+
path: 'hoursDuration',
|
|
125
|
+
expression: truncate(divide(services.field('durationMinutes'), 60)),
|
|
126
|
+
}),
|
|
127
|
+
project({
|
|
128
|
+
path: 'minutesDuration',
|
|
129
|
+
expression: modulo(services.field('durationMinutes'), 60),
|
|
130
|
+
}),
|
|
131
|
+
]
|
|
41
132
|
```
|
|
42
133
|
|
|
43
|
-
|
|
44
|
-
|
|
134
|
+
<code>divide</code> возвращает decimal для точных числовых типов и float64,
|
|
135
|
+
если хотя бы один operand имеет тип float64. <code>truncate</code> отбрасывает
|
|
136
|
+
дробную часть по направлению к нулю и сохраняет scalar type. <code>modulo</code>
|
|
137
|
+
принимает int32, int64 и decimal; float64 отклоняется, чтобы definition одинаково
|
|
138
|
+
работал в поддерживаемых диалектах. Nullability распространяется от operands к
|
|
139
|
+
результату.
|
|
140
|
+
|
|
141
|
+
TypeScript выводит тип и nullability результата во время authoring. Rust при
|
|
142
|
+
регистрации повторно проверяет arity и совместимость типов operands.
|
|
45
143
|
|
|
46
144
|
## Relations
|
|
47
145
|
|
|
@@ -72,7 +170,7 @@ const profileRelation = relation({
|
|
|
72
170
|
```ts
|
|
73
171
|
constraint({
|
|
74
172
|
when: organisationId,
|
|
75
|
-
predicate: eq(users.field('organisationId'),
|
|
173
|
+
predicate: eq(users.field('organisationId'), organisationId),
|
|
76
174
|
})
|
|
77
175
|
|
|
78
176
|
constraint({
|
package/docs/composition.md
CHANGED
|
@@ -19,7 +19,7 @@ Many-связь в одном SQL может размножить root-стро
|
|
|
19
19
|
Child-модуль один раз закрепляет контракт ключа:
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
const itemIds =
|
|
22
|
+
const itemIds = requiredListParam('itemIds', 'int64')
|
|
23
23
|
|
|
24
24
|
const childrenQuery = batchQuery({
|
|
25
25
|
graph: childrenGraph,
|
package/docs/diagnostics.md
CHANGED
|
@@ -45,8 +45,32 @@ Rust выполняет semantic validation:
|
|
|
45
45
|
Такое разделение дает ранние ошибки опечаток, не дублируя planner rules в
|
|
46
46
|
JavaScript.
|
|
47
47
|
|
|
48
|
+
Циклические зависимости relation predicates через <code>exists</code> отклоняются
|
|
49
|
+
при регистрации definition: <code>invalidExistsContext</code> указывает на
|
|
50
|
+
<code>relations[i].on</code>, замыкающий цикл. Параметры, необходимые вложенным
|
|
51
|
+
<code>exists</code>, проверяются в фазе <code>operation</code>; отсутствующий
|
|
52
|
+
параметр получает <code>missingParameter</code> с location
|
|
53
|
+
<code>parameters.<name></code>.
|
|
54
|
+
|
|
55
|
+
Статические параметры batch relation проверяются при <code>composeGraph</code>:
|
|
56
|
+
ошибки относятся к фазе <code>composition</code>, в том числе для имен параметров
|
|
57
|
+
с квадратными скобками. Параметр batch key исключается из этой проверки по точному
|
|
58
|
+
имени; значения ключей проверяются позже, при <code>compileBatch</code>, в фазе
|
|
59
|
+
<code>operation</code>.
|
|
60
|
+
|
|
48
61
|
## Совместимость diagnostics
|
|
49
62
|
|
|
50
|
-
Wire schema
|
|
63
|
+
Wire schema версии 12 добавляет grouped-параметры и scoped-шаблоны `allGroups`.
|
|
64
|
+
Сохранённые raw definitions предыдущей версии требуют обновления; обычные
|
|
65
|
+
DSL-фабрики формируют актуальную версию автоматически. JS DSL и native binding
|
|
66
|
+
должны быть из одного выпуска пакета.
|
|
67
|
+
|
|
68
|
+
Невалидные данные группы получают `invalidParameterType` с точным путём,
|
|
69
|
+
например `parameters.characteristics[0].values[1]`. Ссылка вне своего шаблона
|
|
70
|
+
получает `unknownGroupScope`; повторное объявление активного scope получает
|
|
71
|
+
`shadowedGroupScope`. `allGroups` вне constraint predicate получает
|
|
72
|
+
`invalidAllGroupsContext`. Эти проверки выполняются и для raw wire definitions.
|
|
73
|
+
|
|
74
|
+
Для прежних ошибок сохраняются diagnostic
|
|
51
75
|
codes и locations. Message предназначен человеку и не должен использоваться как
|
|
52
76
|
machine-readable contract.
|
package/docs/sql.md
CHANGED
|
@@ -33,6 +33,41 @@ const oracle = graph.compileOracle(operation, { version: '19c' })
|
|
|
33
33
|
Version capabilities проверяются до возврата SQL. Например, pagination и
|
|
34
34
|
<code>firstBy</code> могут требовать более новую версию диалекта.
|
|
35
35
|
|
|
36
|
+
Semantic functions рендерятся с одинаковым поведением через разные конструкции
|
|
37
|
+
диалектов:
|
|
38
|
+
|
|
39
|
+
| DSL | Oracle | SQL Server |
|
|
40
|
+
| -------------------------------- | ----------------------------- | --------------------------------- |
|
|
41
|
+
| <code>divide(left, right)</code> | <code>left / right</code> | дробное <code>left / right</code> |
|
|
42
|
+
| <code>truncate(value)</code> | <code>TRUNC(value)</code> | <code>ROUND(value, 0, 1)</code> |
|
|
43
|
+
| <code>modulo(left, right)</code> | <code>MOD(left, right)</code> | <code>left % right</code> |
|
|
44
|
+
|
|
45
|
+
SQL Server compiler принудительно переводит <code>divide</code> в дробную
|
|
46
|
+
арифметику, чтобы два integer operands не дали integer division.
|
|
47
|
+
|
|
48
|
+
В SQL Server 2012 и новее <code>concat</code> с одним аргументом компилируется
|
|
49
|
+
с дополнительной пустой строкой <code>''</code>. Списки длиннее 254 аргументов
|
|
50
|
+
разделяются на вложенные вызовы с сохранением порядка. Промежуточные группы
|
|
51
|
+
используют <code>nvarchar(max)</code>, чтобы избежать обрезания строк при
|
|
52
|
+
объединении. <code>NULL</code> обрабатывается как пустая строка согласно
|
|
53
|
+
[семантике CONCAT в SQL Server](https://learn.microsoft.com/en-us/sql/t-sql/functions/concat-transact-sql).
|
|
54
|
+
|
|
55
|
+
## Boolean expressions
|
|
56
|
+
|
|
57
|
+
Компилятор различает значения и условия. Boolean-поле в <code>WHERE</code>,
|
|
58
|
+
<code>ON</code> или логическом выражении превращается в сравнение с <code>1</code>.
|
|
59
|
+
Условие в <code>SELECT</code>, <code>GROUP BY</code> или аргументе функции
|
|
60
|
+
превращается в <code>CASE</code>: <code>TRUE</code> дает <code>1</code>,
|
|
61
|
+
<code>FALSE</code> — <code>0</code>, <code>UNKNOWN</code> — <code>NULL</code>.
|
|
62
|
+
Boolean metadata и TypeScript-типы сохраняются; преобразование значений драйвера
|
|
63
|
+
остается ответственностью executor.
|
|
64
|
+
|
|
65
|
+
В Oracle 23ai условия сохраняют нативный SQL-тип <code>BOOLEAN</code> в контексте
|
|
66
|
+
значения. В контексте условия скалярные значения оборачиваются в
|
|
67
|
+
<code>TO_BOOLEAN</code>, который принимает как числовое, так и boolean-представление.
|
|
68
|
+
Прямые проекции полей и существующие сравнения с boolean literals сохраняют
|
|
69
|
+
прежний SQL.
|
|
70
|
+
|
|
36
71
|
## Bindings
|
|
37
72
|
|
|
38
73
|
Compiler никогда не вставляет operation values в SQL. Каждый binding содержит:
|
|
@@ -40,11 +75,64 @@ Compiler никогда не вставляет operation values в SQL. Каж
|
|
|
40
75
|
- SQL placeholder name;
|
|
41
76
|
- имя parameter;
|
|
42
77
|
- scalar type;
|
|
43
|
-
- index элемента для list parameter
|
|
78
|
+
- index элемента для list parameter;
|
|
79
|
+
- path до вложенного значения для grouped parameter.
|
|
44
80
|
|
|
45
81
|
Повторное использование parameter переиспользует binding. Пустой list
|
|
46
82
|
компилируется в ложное условие без placeholders.
|
|
47
83
|
|
|
84
|
+
Для scalar отсутствуют и `index`, и `path`. Для обычного list сохраняется
|
|
85
|
+
`index`. Для grouped list используется только `path`, например:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
;[
|
|
89
|
+
{ name: 'p0', parameter: 'characteristics', scalarType: 'int64', path: [0, 'key'] },
|
|
90
|
+
{ name: 'p1', parameter: 'characteristics', scalarType: 'int64', path: [0, 'values', 1] },
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Executor извлекает значение из `operation.parameters[binding.parameter]`,
|
|
95
|
+
последовательно проходя по сегментам `path`. `index` и `path` взаимно исключаются;
|
|
96
|
+
отсутствующий сегмент должен приводить к ошибке, а не к подстановке `null`.
|
|
97
|
+
Тип binding соответствует конкретному ключу или элементу values, а не контейнеру.
|
|
98
|
+
Повторное обращение к тому же параметру и пути переиспользует placeholder.
|
|
99
|
+
Потребитель должен обновить resolver bindings до начала использования grouped-параметров.
|
|
100
|
+
|
|
101
|
+
`allGroups` разворачивает шаблон в условия, соединённые `AND`, только при
|
|
102
|
+
компиляции операции. Для отсутствующего optional-параметра и пустого списка
|
|
103
|
+
групп результат равен `TRUE`; зависимости тела шаблона не включаются в план.
|
|
104
|
+
Каждый `exists` остаётся коррелированным подзапросом по описанному пути relations,
|
|
105
|
+
включая `firstBy`. Definition не изменяется, но SQL зависит от количества групп
|
|
106
|
+
и значений. Это один SQL-запрос, а не отдельное выполнение на каждую группу.
|
|
107
|
+
|
|
108
|
+
На один statement допускается до 10 000 разворачиваний тела `allGroups`
|
|
109
|
+
суммарно по всем шаблонам, включая вложенные. Превышение возвращает
|
|
110
|
+
`groupExpansionLimitExceeded`: вложенность может увеличить SQL многократно,
|
|
111
|
+
даже когда все bindings переиспользуются. Лимит применяется отдельно к каждой
|
|
112
|
+
компиляции и не ограничивает каталог доступных групп в приложении.
|
|
113
|
+
|
|
114
|
+
Также ограничено суммарное число элементов параметров-массивов, развёрнутых
|
|
115
|
+
внутри этих шаблонов: до 100 000 на statement. Повторное использование
|
|
116
|
+
одинаковых placeholders считается повторно, поскольку увеличивает SQL.
|
|
117
|
+
Ограничение охватывает и `values` группы, и обычный list parameter внутри
|
|
118
|
+
`allGroups`; превышение даёт `groupValueExpansionLimitExceeded`.
|
|
119
|
+
|
|
120
|
+
SQL Server compiler отклоняет statement с более чем 2100 уникальными bindings
|
|
121
|
+
с диагностикой `tooManyParameters`. Считаются все scalar, list и grouped bindings
|
|
122
|
+
вместе; разбиение `IN` не уменьшает их количество. Для больших выборок нужна
|
|
123
|
+
другая стратегия передачи данных, которой текущий grouped DSL не подменяет
|
|
124
|
+
явную ошибку. Ограничение согласовано с
|
|
125
|
+
[бюджетом параметров SQL Server](https://learn.microsoft.com/sql/sql-server/maximum-capacity-specifications-for-sql-server).
|
|
126
|
+
|
|
127
|
+
В Oracle большие <code>inParam</code> (включая values группы) и <code>inList</code> разбиваются на
|
|
128
|
+
группы <code>IN</code>, объединённые <code>OR</code> в общих скобках внутри
|
|
129
|
+
одного SQL. Размер группы — до 1000 выражений в 11g–21c и до 65535 в 23ai
|
|
130
|
+
согласно [лимитам Oracle](https://python-oracledb.readthedocs.io/en/v2.5.1/user_guide/bind.html#binding-a-large-number-of-items-in-an-in-list).
|
|
131
|
+
<code>not</code> применяется ко всей группе и сохраняет SQL-семантику
|
|
132
|
+
<code>NULL</code>. Порядок элементов и индексы bindings сохраняются, включая
|
|
133
|
+
дубликаты. Пустой литеральный <code>inList</code> по-прежнему отклоняется при
|
|
134
|
+
валидации definition.
|
|
135
|
+
|
|
48
136
|
## Result metadata
|
|
49
137
|
|
|
50
138
|
SQL columns получают технические aliases <code>c0</code>,
|
package/docs/type-system.md
CHANGED
|
@@ -16,6 +16,10 @@ TypeScript помогает автору до регистрации, Rust ос
|
|
|
16
16
|
<code>int64</code> принимает number только в безопасном диапазоне JavaScript.
|
|
17
17
|
Decimal string проверяется как конечная десятичная запись.
|
|
18
18
|
|
|
19
|
+
<code>integer(value)</code> поддерживает весь диапазон от
|
|
20
|
+
<code>Number.MIN_SAFE_INTEGER</code> до <code>Number.MAX_SAFE_INTEGER</code>,
|
|
21
|
+
включая значения за пределами 32 бит.
|
|
22
|
+
|
|
19
23
|
## QueryOperation
|
|
20
24
|
|
|
21
25
|
Definition передает свои параметры, projection paths и ordering names в
|
|
@@ -34,6 +38,49 @@ const operation = {
|
|
|
34
38
|
Required parameters обязательны, optional параметры могут отсутствовать,
|
|
35
39
|
list parameters принимают readonly arrays.
|
|
36
40
|
|
|
41
|
+
Grouped list parameters выводятся как:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
readonly {
|
|
45
|
+
key: ScalarParameterValue<KeyType>
|
|
46
|
+
values: readonly ScalarParameterValue<ValueType>[]
|
|
47
|
+
}[]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Например, <code>requiredGroupsParam('filters', { key: 'string', value: 'int64' })</code>
|
|
51
|
+
требует <code>parameters.filters</code> типа
|
|
52
|
+
<code>readonly { key: string; values: readonly (number | string)[] }[]</code>.
|
|
53
|
+
Optional grouped parameter может отсутствовать, но не заменяться на <code>null</code>.
|
|
54
|
+
Оба массива принимают readonly tuples, в том числе из <code>as const</code>.
|
|
55
|
+
|
|
56
|
+
Контейнер групп, отдельная группа и контейнер <code>values</code> не могут быть
|
|
57
|
+
<code>null</code>. Ключ и элементы списка следуют обычному scalar contract:
|
|
58
|
+
<code>null</code> запрещен для всех типов, кроме <code>json</code>. JSON сохраняет
|
|
59
|
+
существующую семантику JSON values, включая <code>null</code> и вложенные структуры.
|
|
60
|
+
Rust проверяет наличие <code>key</code>/<code>values</code>, неизвестные поля
|
|
61
|
+
группы и scalar types, включая ограничения числовых значений.
|
|
62
|
+
|
|
63
|
+
Grouped parameters сохраняют эти типы при композиции GraphModule и могут быть
|
|
64
|
+
статическими параметрами дочернего <code>batchRelation</code>. Ключ
|
|
65
|
+
<code>batchQuery</code> по-прежнему требует обычный list parameter, не grouped list.
|
|
66
|
+
|
|
67
|
+
В callback <code>allGroups</code> ключ имеет scalar type <code>KeyType</code>,
|
|
68
|
+
а <code>GroupValuesRef<ValueType></code> принимается только как второй аргумент
|
|
69
|
+
<code>inParameter</code>/<code>inParam</code>. TypeScript исключает эту reference
|
|
70
|
+
из scalar expressions и root parameters; Rust проверяет lexical scope и
|
|
71
|
+
совместимость типов сравнения и membership.
|
|
72
|
+
|
|
73
|
+
Для grouped bindings <code>path</code> указывает на значение внутри operation
|
|
74
|
+
parameter: <code>[groupIndex, 'key']</code> или
|
|
75
|
+
<code>[groupIndex, 'values', valueIndex]</code>. Scalar bindings остаются без
|
|
76
|
+
<code>index</code>/<code>path</code>, обычные list bindings сохраняют
|
|
77
|
+
<code>index</code>. Поля <code>index</code> и <code>path</code> взаимоисключающие.
|
|
78
|
+
Это же правило действует для результатов <code>compileBatch</code>.
|
|
79
|
+
|
|
80
|
+
В Node API <code>offset</code> и <code>limit</code> принимают безопасные целые
|
|
81
|
+
JavaScript, включая значения больше <code>2 \*\* 32 - 1</code>.
|
|
82
|
+
<code>offset</code> должен быть неотрицательным, <code>limit</code> — положительным.
|
|
83
|
+
|
|
37
84
|
## Nullability
|
|
38
85
|
|
|
39
86
|
Nullability выражения складывается из:
|
|
@@ -46,6 +93,20 @@ Nullability выражения складывается из:
|
|
|
46
93
|
Required relation под optional ancestor остается nullable. Это свойство
|
|
47
94
|
вычисляется из topology один раз при регистрации.
|
|
48
95
|
|
|
96
|
+
В <code>fieldType</code> флаг <code>nullable</code> типа <code>boolean</code>
|
|
97
|
+
означает, что поле может быть NULL. Литеральные <code>true</code> и
|
|
98
|
+
<code>false</code> позволяют вывести точную nullability.
|
|
99
|
+
|
|
100
|
+
Сравнения <code>eq</code>/<code>neq</code> с гарантированным <code>null</code>
|
|
101
|
+
(включая <code>literal(null)</code>) дают non-null результат. Тип операнда
|
|
102
|
+
<code>string | null</code> сам по себе не гарантирует сравнение именно с NULL:
|
|
103
|
+
nullable поле при сравнении со строкой может дать NULL.
|
|
104
|
+
|
|
105
|
+
<code>allGroups</code> возвращает boolean с nullability predicate шаблона.
|
|
106
|
+
Пустой набор групп дает <code>true</code>, но при наличии групп nullable
|
|
107
|
+
сравнение внутри callback может дать SQL UNKNOWN. Если callback возвращает
|
|
108
|
+
<code>exists</code>, результат остается non-null boolean.
|
|
109
|
+
|
|
49
110
|
## ResultOf
|
|
50
111
|
|
|
51
112
|
```ts
|
|
@@ -77,7 +138,8 @@ compilation не меняются.
|
|
|
77
138
|
Rust проверяет:
|
|
78
139
|
|
|
79
140
|
- ссылки на sources, fields и parameters;
|
|
80
|
-
- scalar/list shape
|
|
141
|
+
- scalar/list/groupedList shape параметров и key type grouped parameters;
|
|
142
|
+
- lexical scopes ключей и списков групп, допустимость binder в constraint context;
|
|
81
143
|
- совместимость сравнений и функций;
|
|
82
144
|
- boolean тип predicates;
|
|
83
145
|
- orderable и groupable expressions;
|