@live-change/simple-query 0.9.138 → 0.9.142
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.md +11 -0
- package/README.md +3 -3
- package/package.json +3 -3
- package/src/autoIndex.db.js +3 -0
- package/src/index.ts +3 -0
- package/src/query.ts +823 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright 2019-2024 Michał Łaszczewski
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
10
|
+
|
|
11
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
CHANGED
|
@@ -27,9 +27,9 @@ const channelMessagesWithUsersAndIdentificationByTime = query({ // definition
|
|
|
27
27
|
code(props, { user, message, identification }) => {
|
|
28
28
|
const { channel, ...range } = props
|
|
29
29
|
message.time.inside(range)
|
|
30
|
-
message.channel.
|
|
31
|
-
user.id.
|
|
32
|
-
identification.id.
|
|
30
|
+
message.channel.eqals(channel)
|
|
31
|
+
user.id.equals(message.au thor)
|
|
32
|
+
identification.id.equals(user.id)
|
|
33
33
|
}
|
|
34
34
|
})
|
|
35
35
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@live-change/simple-query",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.142",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"type": "module",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@live-change/framework": "0.9.
|
|
25
|
+
"@live-change/framework": "^0.9.142",
|
|
26
26
|
"pluralize": "^8.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"typedoc-plugin-markdown": "^4.6.3",
|
|
31
31
|
"typedoc-plugin-rename-defaults": "^0.7.3"
|
|
32
32
|
},
|
|
33
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "efc249d458250a4d1cb9bd5ff847de066452dc1c"
|
|
34
34
|
}
|
package/src/index.ts
ADDED
package/src/query.ts
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PropertyDefinitionSpecification, ServiceDefinition, ServiceDefinitionSpecification
|
|
3
|
+
} from "@live-change/framework"
|
|
4
|
+
|
|
5
|
+
import { ModelDefinition, ForeignModelDefinition } from "@live-change/framework"
|
|
6
|
+
|
|
7
|
+
import { PropertyDefinition } from "@live-change/framework"
|
|
8
|
+
|
|
9
|
+
interface Range {
|
|
10
|
+
gt?: string
|
|
11
|
+
gte?: string
|
|
12
|
+
lt?: string
|
|
13
|
+
lte?: string,
|
|
14
|
+
reverse?: boolean,
|
|
15
|
+
limit?: number,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface QueryParameters {
|
|
19
|
+
[key: string]: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface QueryInputs {
|
|
23
|
+
[key: string]: QueryInput
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class CanBeStatic {
|
|
27
|
+
$_markStatic() {
|
|
28
|
+
this[staticRuleSymbol] = true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
$_isStatic() {
|
|
32
|
+
return this[staticRuleSymbol]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class RuleSource {
|
|
37
|
+
rule: QueryRule
|
|
38
|
+
input: QueryInput
|
|
39
|
+
type: string
|
|
40
|
+
dependentBy: RuleSource[]
|
|
41
|
+
dependsOn: RuleSource[]
|
|
42
|
+
index: IndexInfo | null
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
constructor(rule: QueryRule, input: QueryInput, source: QuerySource, type: string) {
|
|
46
|
+
this.rule = rule
|
|
47
|
+
this.input = input
|
|
48
|
+
this.type = type
|
|
49
|
+
this.dependentBy = []
|
|
50
|
+
this.dependsOn = []
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class QueryRule extends CanBeStatic {
|
|
56
|
+
$_hasStaticParameter() {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$_getSources(): RuleSource[] {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
$_equals(other: QueryRule) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$_parametersJSON(resultParameters: string[]) {
|
|
69
|
+
return undefined
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
type QueryRules = QueryRule[]
|
|
74
|
+
|
|
75
|
+
type QueryCode = ((parameters: QueryParameters, inputs: QueryInputs) => QueryRules)
|
|
76
|
+
|
|
77
|
+
type QuerySource = ModelDefinition<any> | ForeignModelDefinition | any /// Query Definition will be recursive definition, so use any for now
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
interface QueryDefinitionSpecification {
|
|
81
|
+
name: string
|
|
82
|
+
properties: Record<string, PropertyDefinitionSpecification>
|
|
83
|
+
returns?: PropertyDefinitionSpecification,
|
|
84
|
+
sources: Record<string, QuerySource>,
|
|
85
|
+
code: QueryCode,
|
|
86
|
+
sourceName: string,
|
|
87
|
+
update: boolean,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class OutputMapping {
|
|
91
|
+
result: string
|
|
92
|
+
path: string[]
|
|
93
|
+
alias: string
|
|
94
|
+
|
|
95
|
+
constructor(result: string, path: string[], alias: string) {
|
|
96
|
+
this.result = result
|
|
97
|
+
this.path = path
|
|
98
|
+
this.alias = alias
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getDescription(indent: string = "") {
|
|
102
|
+
return `OutputMapping(${this.result}.${this.path.join(".")}, ${this.alias})`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
equals(other: OutputMapping) {
|
|
106
|
+
return this.result === other.result && this.path.join(".") === other.path.join(".") && this.alias === other.alias
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
class IndexInfo {
|
|
111
|
+
singleSource: QuerySource | null = null
|
|
112
|
+
rules: QueryRule[]
|
|
113
|
+
indexParts: OutputMapping[]
|
|
114
|
+
name: string
|
|
115
|
+
|
|
116
|
+
constructor(rules: QueryRule[], indexParts: OutputMapping[], name: string = undefined) {
|
|
117
|
+
this.rules = rules
|
|
118
|
+
this.indexParts = indexParts
|
|
119
|
+
const allSources = rules.map(rule => rule.$_getSources()).flat()
|
|
120
|
+
const firstSource = allSources[0]
|
|
121
|
+
if(allSources.every(source => source === firstSource)) this.singleSource = firstSource
|
|
122
|
+
if(this.singleSource) {
|
|
123
|
+
this.name = name || (this.singleSource.input.$source.getTypeName() + "_" + (indexParts
|
|
124
|
+
.map(part => part.path.join(".")).join("_")))
|
|
125
|
+
} else {
|
|
126
|
+
this.name = name || indexParts
|
|
127
|
+
.map(part => {
|
|
128
|
+
const source = allSources.find(source => source.input.$alias === part.result)
|
|
129
|
+
if(!source) throw new Error("Source not found for index part: " + part)
|
|
130
|
+
return source.input.$source.getTypeName() + "_" + part.path.join(".")
|
|
131
|
+
}).join("_")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
equals(other: IndexInfo) {
|
|
136
|
+
if(this.rules.length !== other.rules.length) return false
|
|
137
|
+
if(this.name !== other.name) return false
|
|
138
|
+
if(this.indexParts.length !== other.indexParts.length) return false
|
|
139
|
+
if(this.indexParts.some((part, i) => part !== other.indexParts[i])) return false
|
|
140
|
+
for(let i = 0; i < this.rules.length; i++) {
|
|
141
|
+
if(this.rules[i].$_equals(other.rules[i])) return false
|
|
142
|
+
}
|
|
143
|
+
return true
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getDescription(indent: string = "") {
|
|
147
|
+
return `Index(${this.name}, `+
|
|
148
|
+
((this.rules.length > 0)
|
|
149
|
+
? `\n${indent} ${this.rules.map(rule => queryDescription(rule, indent + " ")).join("\n "+indent)}`
|
|
150
|
+
: "")+
|
|
151
|
+
`\n${indent} ${this.indexParts.map(part => part.getDescription(indent + " ")).join("\n "+indent)}`+
|
|
152
|
+
`\n${indent})`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
$_executionJSON() {
|
|
156
|
+
if(!this.singleSource) throw new Error("Indexes with multiple sources are not supported")
|
|
157
|
+
return {
|
|
158
|
+
sourceType: "index",
|
|
159
|
+
name: this.name,
|
|
160
|
+
alias: this.singleSource.input.$alias + 'Indexed'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const staticRuleSymbol = Symbol("static")
|
|
166
|
+
|
|
167
|
+
type ExecutionStep = {
|
|
168
|
+
execution: any,
|
|
169
|
+
next: ExecutionStep[]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class QueryDefinition<SDS extends ServiceDefinitionSpecification> {
|
|
173
|
+
|
|
174
|
+
service: ServiceDefinition<SDS>
|
|
175
|
+
definition: QueryDefinitionSpecification
|
|
176
|
+
properties: Record<string, PropertyDefinition<any>>
|
|
177
|
+
rules: QueryRules
|
|
178
|
+
firstRule: QueryRule
|
|
179
|
+
rootSources: RuleSource[]
|
|
180
|
+
ruleSources: RuleSource[]
|
|
181
|
+
indexes: IndexInfo[]
|
|
182
|
+
|
|
183
|
+
executionPlan: ExecutionStep[]
|
|
184
|
+
indexPlan: ExecutionStep[]
|
|
185
|
+
|
|
186
|
+
constructor(serviceDefinition: ServiceDefinition<SDS>, definition: QueryDefinitionSpecification) {
|
|
187
|
+
this.service = serviceDefinition
|
|
188
|
+
this.definition = definition
|
|
189
|
+
|
|
190
|
+
this.properties = Object.fromEntries(
|
|
191
|
+
Object.entries(definition.properties)
|
|
192
|
+
.map(
|
|
193
|
+
([propertyName, propertyDefinition]) => [propertyName, new PropertyDefinition(propertyDefinition)]
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
this.computeRules()
|
|
198
|
+
this.markStaticRules()
|
|
199
|
+
|
|
200
|
+
this.printRules()
|
|
201
|
+
|
|
202
|
+
this.computeDependencies()
|
|
203
|
+
this.computeIndexes()
|
|
204
|
+
|
|
205
|
+
this.printDependencies()
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
printRules() {
|
|
210
|
+
console.log("QUERY RULES:")
|
|
211
|
+
for(const key in this.rules) {
|
|
212
|
+
console.log(` ${key}:`, queryDescription(this.rules[key], ' '))
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
computeRules() {
|
|
217
|
+
const queryProperties = {}
|
|
218
|
+
for(const propertyName in this.definition.properties) {
|
|
219
|
+
const propertyDefinition = this.definition.properties[propertyName]
|
|
220
|
+
const base = new QueryPropertyBase([propertyName])
|
|
221
|
+
queryProperties[propertyName] = createQueryPropertyProxy(base, propertyName, this.properties[propertyName])
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const queryInputs = {}
|
|
225
|
+
for(const sourceName in this.definition.sources) {
|
|
226
|
+
const propertyDefinition = this.definition.sources[sourceName]
|
|
227
|
+
const base = new QueryInputBase(this.definition.sources[sourceName], [], sourceName)
|
|
228
|
+
queryInputs[sourceName] = createQueryInputProxy(base)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// run the code to collect relations
|
|
232
|
+
this.rules = this.definition.code(queryProperties, queryInputs)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
markStaticRules() {
|
|
236
|
+
for(const key in this.rules) {
|
|
237
|
+
const rule = this.rules[key]
|
|
238
|
+
markStatic(rule)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
computeDependencies() {
|
|
243
|
+
const independentRules = this.rules.filter(rule => rule.$_hasStaticParameter())
|
|
244
|
+
if(independentRules.length > 1) {
|
|
245
|
+
console.error("Independent rules:")
|
|
246
|
+
for(const rule of independentRules) {
|
|
247
|
+
console.error(' ' + queryDescription(rule, ' '))
|
|
248
|
+
}
|
|
249
|
+
throw new Error("Multiple independent rules are not supported")
|
|
250
|
+
}
|
|
251
|
+
this.firstRule = independentRules[0] ?? this.rules[0]
|
|
252
|
+
this.rootSources = this.firstRule.$_getSources()
|
|
253
|
+
const providedSources = this.rootSources.slice()
|
|
254
|
+
|
|
255
|
+
const otherSources = []
|
|
256
|
+
for(const rule of this.rules) {
|
|
257
|
+
if(rule === this.firstRule) continue
|
|
258
|
+
const ruleSources = rule.$_getSources()
|
|
259
|
+
for(const ruleSource of ruleSources) {
|
|
260
|
+
otherSources.push(ruleSource)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.ruleSources = this.rootSources.concat(otherSources)
|
|
265
|
+
|
|
266
|
+
while(otherSources.length > 0) {
|
|
267
|
+
/// find sources to link
|
|
268
|
+
let linked = false
|
|
269
|
+
for(const ruleSource of otherSources) {
|
|
270
|
+
for(const providedSource of providedSources) {
|
|
271
|
+
if(ruleSource == providedSource) {
|
|
272
|
+
throw new Error("Rule source is equal to provided source")
|
|
273
|
+
}
|
|
274
|
+
/* console.log("RULE SOURCE", queryDescription(ruleSource.source, ' '))
|
|
275
|
+
console.log("PROVIDED SOURCE", queryDescription(providedSource.source, ' '))
|
|
276
|
+
console.log("MATCH", ruleSource.source === providedSource.source) */
|
|
277
|
+
if(ruleSource.input.$_canBeUsedAsSource(providedSource.input)) {
|
|
278
|
+
ruleSource.dependsOn.push(providedSource)
|
|
279
|
+
providedSource.dependentBy.push(ruleSource)
|
|
280
|
+
otherSources.splice(otherSources.indexOf(ruleSource), 1)
|
|
281
|
+
|
|
282
|
+
for(const otherRuleSource of this.ruleSources) {
|
|
283
|
+
if(otherRuleSource.rule !== ruleSource.rule) continue
|
|
284
|
+
if(otherRuleSource === ruleSource) continue
|
|
285
|
+
if(providedSources.find(s => s === otherRuleSource)) continue
|
|
286
|
+
providedSources.push(otherRuleSource)
|
|
287
|
+
otherSources.splice(otherSources.indexOf(otherRuleSource), 1)
|
|
288
|
+
}
|
|
289
|
+
linked = true
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
if(linked) break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if(!linked && otherSources.length > 0) {
|
|
296
|
+
console.error("Impossible to link query, found independent sources:")
|
|
297
|
+
for(const ruleSource of otherSources) {
|
|
298
|
+
console.error(' ' + queryDescription(ruleSource, ' '))
|
|
299
|
+
}
|
|
300
|
+
throw new Error("Impossible to link query, found independent sources")
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
printDependencies(indent: string = '') {
|
|
306
|
+
console.log('QUERY DEPENDENCIES:')
|
|
307
|
+
for(const key in this.rules) {
|
|
308
|
+
const rule = this.rules[key]
|
|
309
|
+
console.log(`${indent} RULE ${key}:`)
|
|
310
|
+
console.log(`${indent} ${queryDescription(rule, indent + ' ')}`)
|
|
311
|
+
console.log(`${indent} SOURCES:`)
|
|
312
|
+
for(const ruleSource of this.ruleSources) {
|
|
313
|
+
if(ruleSource.rule !== rule) continue
|
|
314
|
+
console.log(`${indent} ${queryDescription(ruleSource.input.$source, indent + ' ')} as ${ruleSource.input.$alias}`)
|
|
315
|
+
if(ruleSource.dependsOn.length > 0) {
|
|
316
|
+
console.log(`${indent} DEPENDS ON: `+
|
|
317
|
+
`${ruleSource.dependsOn.map(d => this.rules.indexOf(d.rule)).join(', ')}`
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
if(ruleSource.dependentBy.length > 0) {
|
|
321
|
+
console.log(`${indent} DEPENDENT BY: `+
|
|
322
|
+
`${ruleSource.dependentBy.map(d => this.rules.indexOf(d.rule)).join(', ')}`
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
if(ruleSource.index) {
|
|
326
|
+
console.log(`${indent} ${ruleSource.index.getDescription(indent + ' ')}`)
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
computeIndexes() {
|
|
333
|
+
this.indexes = []
|
|
334
|
+
for(const ruleSource of this.ruleSources) {
|
|
335
|
+
const potentialIndex = ruleSource.input.$_getIndexInfo(this.indexes)
|
|
336
|
+
if(!potentialIndex) continue
|
|
337
|
+
const existingIndex = this.indexes.find(index => index.equals(potentialIndex))
|
|
338
|
+
if(existingIndex) {
|
|
339
|
+
ruleSource.index = existingIndex
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
this.indexes.push(potentialIndex)
|
|
343
|
+
ruleSource.index = potentialIndex
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
computeSourceExecutionPlan(source: RuleSource, resultParameters: string[]) {
|
|
348
|
+
/// TODO: W poniższej linii jest bug, powinno pobierać odmienne source, a nie te bezpośrednio zależne.
|
|
349
|
+
/// Poza tym przy pobieraniu wstecz nie koniecznie potrzeba indexów,
|
|
350
|
+
/// może trzeba wprowadzić index-forward i index-backward ?
|
|
351
|
+
/// A może trzeba wprowadzić analizę tego co mamy i co chcemy uzyskać w przyszłości ?
|
|
352
|
+
const next = source.dependentBy.map(dependent => {
|
|
353
|
+
const otherSource = this.ruleSources.find(s => s.rule === dependent.rule && s != dependent)
|
|
354
|
+
return this.computeSourceExecutionPlan(otherSource, [...resultParameters, source.input.$alias])
|
|
355
|
+
})
|
|
356
|
+
const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
|
|
357
|
+
if(source.index) {
|
|
358
|
+
const indexExecution = {
|
|
359
|
+
...source.index.$_executionJSON(),
|
|
360
|
+
by: ruleParameters[Object.keys(ruleParameters)[0]]
|
|
361
|
+
}
|
|
362
|
+
const indexNext = [{
|
|
363
|
+
operation: 'object',
|
|
364
|
+
...source.input.$_executionJSON(),
|
|
365
|
+
by: {
|
|
366
|
+
type: 'result',
|
|
367
|
+
path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
|
|
368
|
+
},
|
|
369
|
+
next
|
|
370
|
+
}]
|
|
371
|
+
return {
|
|
372
|
+
execution: indexExecution,
|
|
373
|
+
next: indexNext
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const execution = {
|
|
377
|
+
...source.input.$_executionJSON(),
|
|
378
|
+
by: ruleParameters[Object.keys(ruleParameters)[0]]
|
|
379
|
+
}
|
|
380
|
+
const executionPlan = {
|
|
381
|
+
execution,
|
|
382
|
+
next
|
|
383
|
+
}
|
|
384
|
+
return executionPlan
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
computeExecutionPlan() {
|
|
388
|
+
this.executionPlan = []
|
|
389
|
+
for(const rootSource of this.rootSources) {
|
|
390
|
+
this.executionPlan.push(this.computeSourceExecutionPlan(rootSource, []))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
computeSourceIndexPlan(source: RuleSource, resultParameters: string[], processed: RuleSource[], allProcessed: RuleSource[]) {
|
|
395
|
+
allProcessed.push(source)
|
|
396
|
+
const ruleSources = this.ruleSources
|
|
397
|
+
.filter(s => s.input.$source === source.input.$source && s != source && !processed.includes(s))
|
|
398
|
+
const next = ruleSources.map(ruleSource => {
|
|
399
|
+
const otherSource = this.ruleSources.find(s => s.rule === ruleSource.rule && s != ruleSource)
|
|
400
|
+
if(!otherSource) return null
|
|
401
|
+
if(processed.includes(otherSource)) return null
|
|
402
|
+
return this.computeSourceIndexPlan(
|
|
403
|
+
otherSource,
|
|
404
|
+
[...resultParameters, source.input.$alias],
|
|
405
|
+
[...processed, source],
|
|
406
|
+
allProcessed
|
|
407
|
+
)
|
|
408
|
+
}).filter(s => s !== null)
|
|
409
|
+
const ruleParameters = JSON.parse(JSON.stringify(source.rule.$_parametersJSON(resultParameters)))
|
|
410
|
+
if(source.index) {
|
|
411
|
+
const indexExecution = {
|
|
412
|
+
...source.index.$_executionJSON(),
|
|
413
|
+
by: ruleParameters[Object.keys(ruleParameters)[0]]
|
|
414
|
+
}
|
|
415
|
+
const indexNext = [{
|
|
416
|
+
operation: 'object',
|
|
417
|
+
...source.input.$_executionJSON(),
|
|
418
|
+
by: {
|
|
419
|
+
type: 'result',
|
|
420
|
+
path: [indexExecution.alias, source.index.indexParts.at(-1).alias],
|
|
421
|
+
},
|
|
422
|
+
next
|
|
423
|
+
}]
|
|
424
|
+
return {
|
|
425
|
+
execution: indexExecution,
|
|
426
|
+
next: indexNext
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const execution = {
|
|
430
|
+
...source.input.$_executionJSON(),
|
|
431
|
+
by: ruleParameters[Object.keys(ruleParameters)[0]],
|
|
432
|
+
}
|
|
433
|
+
const executionPlan = {
|
|
434
|
+
execution,
|
|
435
|
+
next
|
|
436
|
+
}
|
|
437
|
+
return executionPlan
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
computeIndexPlan() {
|
|
441
|
+
const indexSources = []
|
|
442
|
+
for(const ruleSource of this.ruleSources) {
|
|
443
|
+
const source = ruleSource.input.$source
|
|
444
|
+
if(indexSources.includes(source)) continue
|
|
445
|
+
indexSources.push(source)
|
|
446
|
+
}
|
|
447
|
+
this.indexPlan = indexSources.map(source => {
|
|
448
|
+
const firstFetch = {
|
|
449
|
+
sourceType: sourceType(source),
|
|
450
|
+
name: source.getTypeName(),
|
|
451
|
+
alias: source.getTypeName(),
|
|
452
|
+
by: { type: 'object', properties: {} } /// infinite range
|
|
453
|
+
}
|
|
454
|
+
const ruleSources = this.ruleSources.filter(s => s.input.$source === source)
|
|
455
|
+
const ruleSourcesAliases = Array.from(new Set(ruleSources.map(s => s.input.$alias)))
|
|
456
|
+
const next:ExecutionStep[] = ruleSourcesAliases.map((alias) => {
|
|
457
|
+
const processed = []
|
|
458
|
+
|
|
459
|
+
const aliasRuleSources = ruleSources.filter(s => s.input.$alias === alias)
|
|
460
|
+
const next:ExecutionStep[] = aliasRuleSources.map((input) => {
|
|
461
|
+
const rule = input.rule
|
|
462
|
+
const output = this.ruleSources.find(s => s.rule === rule && s != input)
|
|
463
|
+
if(!output) return null
|
|
464
|
+
const ignoredSources = ruleSources.filter(s => s.input.$source === source && s.input.$alias === input.input.$alias)
|
|
465
|
+
const executionPlan = this.computeSourceIndexPlan(output, [firstFetch.alias], [input, ...ignoredSources], processed)
|
|
466
|
+
executionPlan.execution.by = { type: 'result', path: [firstFetch.alias, ...input.input.$path] }
|
|
467
|
+
return {
|
|
468
|
+
...executionPlan,
|
|
469
|
+
}
|
|
470
|
+
}).filter(s => s !== null)
|
|
471
|
+
|
|
472
|
+
const mapping = {
|
|
473
|
+
[alias]: [firstFetch.alias],
|
|
474
|
+
}
|
|
475
|
+
for(const processedSource of processed) {
|
|
476
|
+
mapping[processedSource.input.$alias] = [processedSource.input.$alias]
|
|
477
|
+
}
|
|
478
|
+
const execution = {
|
|
479
|
+
operation: 'output',
|
|
480
|
+
mapping
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
execution,
|
|
484
|
+
next
|
|
485
|
+
}
|
|
486
|
+
}).filter(s => s !== null)
|
|
487
|
+
return {
|
|
488
|
+
execution: firstFetch,
|
|
489
|
+
next
|
|
490
|
+
}
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
prepareQuery() {
|
|
495
|
+
console.log("CREATE INDEXES", this.indexes)
|
|
496
|
+
|
|
497
|
+
process.exit(0)
|
|
498
|
+
|
|
499
|
+
this.computeExecutionPlan()
|
|
500
|
+
console.log("EXECUTION PLAN:")
|
|
501
|
+
console.log(JSON.stringify(this.executionPlan, null, 2))
|
|
502
|
+
/// TODO: create indexes used by query
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
/// TODO: prepare query
|
|
506
|
+
|
|
507
|
+
process.exit(0)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
createIndex() {
|
|
511
|
+
this.computeIndexPlan()
|
|
512
|
+
/// TODO: create index from query
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
export type QueryFactoryFunction<SDS extends ServiceDefinitionSpecification> =
|
|
518
|
+
(definition: QueryDefinitionSpecification) => QueryDefinition<SDS>
|
|
519
|
+
|
|
520
|
+
export default function queryFactory<SDS extends ServiceDefinitionSpecification>(
|
|
521
|
+
serviceDefinition: ServiceDefinition<SDS>
|
|
522
|
+
) {
|
|
523
|
+
const queryFactoryFunction: QueryFactoryFunction<SDS> =
|
|
524
|
+
(definition: QueryDefinitionSpecification) => {
|
|
525
|
+
const query = new QueryDefinition<SDS>(serviceDefinition, definition)
|
|
526
|
+
query.prepareQuery()
|
|
527
|
+
return query
|
|
528
|
+
}
|
|
529
|
+
return queryFactoryFunction
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
type RuleInput = QueryInputBase | QueryPropertyBase | any
|
|
533
|
+
|
|
534
|
+
function getSource(input: RuleInput): QuerySource {
|
|
535
|
+
if(input instanceof QueryInputBase) return input.$source
|
|
536
|
+
return null
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function isStatic(element: any) {
|
|
540
|
+
if(typeof element !== "object" || element === null) return true
|
|
541
|
+
return element instanceof CanBeStatic ? element.$_isStatic() :
|
|
542
|
+
(element.constructor.name === "Object" ? element[staticRuleSymbol] : false)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function queryDescription(element: any, indent: string = "") {
|
|
546
|
+
const flags = isStatic(element) ? "static " : ""
|
|
547
|
+
if(typeof element.toQueryDescription === "function")
|
|
548
|
+
return flags + element.toQueryDescription(indent)
|
|
549
|
+
const fields = Object.entries(element)
|
|
550
|
+
.map(([key, value]) => `${indent} ${key}: ${queryDescription(value, indent + " ")}`)
|
|
551
|
+
.join("\n")
|
|
552
|
+
if(element.constructor.name !== "Object") return flags + `${element.constructor.name}(${fields})`
|
|
553
|
+
return flags + '{\n'+fields+`\n${indent}}`;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function markStatic(element: any) {
|
|
557
|
+
if(element instanceof CanBeStatic) return element.$_markStatic()
|
|
558
|
+
if(typeof element === "object" && element !== null) {
|
|
559
|
+
let allStatic = true
|
|
560
|
+
for(const key in element) {
|
|
561
|
+
markStatic(element[key])
|
|
562
|
+
if(!isStatic(element[key])) allStatic = false
|
|
563
|
+
}
|
|
564
|
+
if(allStatic) element[staticRuleSymbol] = true
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function parameterJSON(element: any) {
|
|
570
|
+
if(typeof element !== "object" || element === null) return element
|
|
571
|
+
if(element instanceof QueryPropertyBase) return { property: element.$path }
|
|
572
|
+
const output = {
|
|
573
|
+
type: 'object',
|
|
574
|
+
properties: {}
|
|
575
|
+
}
|
|
576
|
+
for(const key in element) {
|
|
577
|
+
output.properties[key] = parameterJSON(element[key])
|
|
578
|
+
}
|
|
579
|
+
return output
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function parametersJSONForInput(input: RuleInput, resultParameters: string[]) {
|
|
583
|
+
if(isStatic(input)) {
|
|
584
|
+
return parameterJSON(input)
|
|
585
|
+
} else {
|
|
586
|
+
const resultParameter = resultParameters.find(p => p === input.$alias)
|
|
587
|
+
if(resultParameter) {
|
|
588
|
+
return {
|
|
589
|
+
type: 'result',
|
|
590
|
+
path: [resultParameter, ...input.$path],
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export class RangeRule extends QueryRule {
|
|
597
|
+
$input: RuleInput
|
|
598
|
+
$range: RuleInput
|
|
599
|
+
|
|
600
|
+
constructor(input: QueryInputBase, range: Range) {
|
|
601
|
+
super()
|
|
602
|
+
this.$input = input
|
|
603
|
+
this.$range = range
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
toQueryDescription(indent: string = "") {
|
|
607
|
+
return `Range(`+
|
|
608
|
+
`\n${indent} ${queryDescription(this.$input, indent + " ")}`+
|
|
609
|
+
`\n${indent} ${queryDescription(this.$range, indent + " ")}`+
|
|
610
|
+
`\n${indent})`
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
$_markStatic() {
|
|
614
|
+
markStatic(this.$input)
|
|
615
|
+
markStatic(this.$range)
|
|
616
|
+
//console.log("MARK STATIC", queryDescription(this.$input), queryDescription(this.$range), isStatic(this.$input), isStatic(this.$range))
|
|
617
|
+
this[staticRuleSymbol] = isStatic(this.$input) && isStatic(this.$range)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
$_isStatic() {
|
|
621
|
+
return this[staticRuleSymbol]
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
$_hasStaticParameter() {
|
|
625
|
+
return isStatic(this.$input) || isStatic(this.$range)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
$_getSources(): RuleSource[] {
|
|
629
|
+
return [
|
|
630
|
+
new RuleSource(this, this.$input, getSource(this.$input), 'range')
|
|
631
|
+
].filter(s => s.input != null)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
$_parametersJSON(resultParameters: string[]) {
|
|
635
|
+
return {
|
|
636
|
+
input: parametersJSONForInput(this.$input, resultParameters),
|
|
637
|
+
range: parametersJSONForInput(this.$range, resultParameters),
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export class EqualsRule extends QueryRule {
|
|
643
|
+
$inputA: RuleInput
|
|
644
|
+
$inputB: RuleInput
|
|
645
|
+
|
|
646
|
+
constructor(inputA: RuleInput, inputB: RuleInput) {
|
|
647
|
+
super()
|
|
648
|
+
this.$inputA = inputA
|
|
649
|
+
this.$inputB = inputB
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
toQueryDescription(indent: string = "") {
|
|
653
|
+
return `Equals(`+
|
|
654
|
+
`\n${indent} ${queryDescription(this.$inputA, indent + " ")}`+
|
|
655
|
+
`\n${indent} ${queryDescription(this.$inputB, indent + " ")}`+
|
|
656
|
+
`\n${indent})`
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
$_markStatic() {
|
|
660
|
+
markStatic(this.$inputA)
|
|
661
|
+
markStatic(this.$inputB)
|
|
662
|
+
this[staticRuleSymbol] = isStatic(this.$inputA) && isStatic(this.$inputB)
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
$_isStatic() {
|
|
666
|
+
return this[staticRuleSymbol]
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
$_hasStaticParameter() {
|
|
670
|
+
return isStatic(this.$inputA) || isStatic(this.$inputB)
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
$_getSources(): RuleSource[] {
|
|
674
|
+
return [
|
|
675
|
+
new RuleSource(this, this.$inputA, getSource(this.$inputA), 'object'),
|
|
676
|
+
new RuleSource(this, this.$inputB, getSource(this.$inputB), 'object')
|
|
677
|
+
].filter(s => s.input != null)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
$_parametersJSON(resultParameters: string[]) {
|
|
681
|
+
return {
|
|
682
|
+
inputA: parametersJSONForInput(this.$inputA, resultParameters),
|
|
683
|
+
inputB: parametersJSONForInput(this.$inputB, resultParameters),
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function sourceType(source: QuerySource) {
|
|
689
|
+
return (source instanceof ModelDefinition || source instanceof ForeignModelDefinition)
|
|
690
|
+
? "table" : "index"
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export class QueryInputBase extends CanBeStatic {
|
|
694
|
+
$source: QuerySource
|
|
695
|
+
$path: string[]
|
|
696
|
+
$alias: string
|
|
697
|
+
|
|
698
|
+
$inside(range: Range) {
|
|
699
|
+
return new RangeRule(this, range)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
$equals(value: any) {
|
|
703
|
+
return new EqualsRule(this, value)
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
$as(alias: string) {
|
|
707
|
+
return createQueryInputProxy(new QueryInputBase(this.$source, this.$path, alias))
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
constructor(source: QuerySource, path: string[], alias: string = undefined) {
|
|
711
|
+
super()
|
|
712
|
+
this.$source = source
|
|
713
|
+
this.$path = path
|
|
714
|
+
this.$alias = alias
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
toQueryDescription(indent: string = "") {
|
|
718
|
+
return `QueryInput(\n${indent} source: ${queryDescription(this.$source, indent + " ")}`+
|
|
719
|
+
`\n${indent} path: ${this.$path.join(".")}`+
|
|
720
|
+
`\n${indent} alias: ${this.$alias}`+
|
|
721
|
+
`\n${indent})`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
$_markStatic() {
|
|
725
|
+
/// ignore - QueryInput is not static
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
$_isStatic() {
|
|
729
|
+
return false
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
$_canBeUsedAsSource(input: QueryInputBase) {
|
|
733
|
+
return this.$source === input.$source && this.$alias === input.$alias
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
$_getIndexInfo(indexes: IndexInfo[]): IndexInfo | null {
|
|
737
|
+
if(this.$path.length === 0) return null // id is used
|
|
738
|
+
if(this.$path.length === 1 && this.$path[0] === "id") return null // id is used
|
|
739
|
+
return new IndexInfo([
|
|
740
|
+
new RangeRule(this, {}),
|
|
741
|
+
], [
|
|
742
|
+
new OutputMapping(this.$alias, this.$path, this.$path[this.$path.length - 1]),
|
|
743
|
+
new OutputMapping(this.$alias, ['id'], 'to')
|
|
744
|
+
])
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
$_executionJSON() {
|
|
748
|
+
return {
|
|
749
|
+
sourceType: sourceType(this.$source),
|
|
750
|
+
name: this.$source.getTypeName(),
|
|
751
|
+
path: [...this.$path],
|
|
752
|
+
alias: this.$alias
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
$_equals(other: QueryInputBase) {
|
|
756
|
+
return this.$source === other.$source && this.$path.join(".") === other.$path.join(".") && this.$alias === other.$alias
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
export class QueryInput extends QueryInputBase {
|
|
761
|
+
[key: string]: QueryInputBase | any /// Proxy class will be added to this
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
export function createQueryInputProxy(
|
|
766
|
+
base: QueryInputBase
|
|
767
|
+
) {
|
|
768
|
+
return new Proxy(base, {
|
|
769
|
+
get(target, prop, receiver) {
|
|
770
|
+
const foundInBase = Reflect.get(target, prop, receiver)
|
|
771
|
+
if(foundInBase) return foundInBase
|
|
772
|
+
const newBase = new QueryInputBase(base.$source, [...base.$path, prop as string], base.$alias)
|
|
773
|
+
const inputProxy = createQueryInputProxy(newBase)
|
|
774
|
+
return inputProxy
|
|
775
|
+
}
|
|
776
|
+
})
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
export class QueryPropertyBase extends CanBeStatic {
|
|
780
|
+
$path: string[]
|
|
781
|
+
|
|
782
|
+
constructor(path: string[]) {
|
|
783
|
+
super()
|
|
784
|
+
this.$path = path
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
toQueryDescription(indent: string = "") {
|
|
788
|
+
return `QueryProperty(${this.$path.join(".")})`
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
$_isStatic() {
|
|
792
|
+
return true
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
$_markStatic() {
|
|
796
|
+
/// ignore - QueryProperty is always static
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
$_executionJSON() {
|
|
800
|
+
return {
|
|
801
|
+
type: "property",
|
|
802
|
+
path: this.$path
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
export class QueryProperty extends QueryPropertyBase {
|
|
808
|
+
[key: string]: QueryPropertyBase | any /// Proxy class will be added to this
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function createQueryPropertyProxy(
|
|
812
|
+
base: QueryPropertyBase, propertyName: string, propertyDefinition: PropertyDefinition<any>
|
|
813
|
+
) {
|
|
814
|
+
return new Proxy(base, {
|
|
815
|
+
get(target, prop, receiver) {
|
|
816
|
+
const foundInBase = Reflect.get(target, prop, receiver)
|
|
817
|
+
if(foundInBase) return foundInBase
|
|
818
|
+
const propertyBase = new QueryPropertyBase([...base.$path, propertyName])
|
|
819
|
+
const propertyProxy = createQueryPropertyProxy(propertyBase, propertyName, propertyDefinition)
|
|
820
|
+
return propertyProxy
|
|
821
|
+
}
|
|
822
|
+
})
|
|
823
|
+
}
|