@bjornpagen/bumbledb 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COOKBOOK.md +114 -11
- package/README.md +52 -9
- package/dist/capacity.d.ts +22 -59
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +66 -13
- package/dist/capacity.js.map +1 -1
- package/dist/changes.d.ts +26 -3
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +89 -44
- package/dist/changes.js.map +1 -1
- package/dist/closed.d.ts +23 -51
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +110 -168
- package/dist/closed.js.map +1 -1
- package/dist/codec.d.ts +4 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +108 -183
- package/dist/codec.js.map +1 -1
- package/dist/compile.d.ts +16 -16
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +36 -15
- package/dist/compile.js.map +1 -1
- package/dist/db-native.d.ts +39 -0
- package/dist/db-native.d.ts.map +1 -1
- package/dist/db-native.js +8 -0
- package/dist/db-native.js.map +1 -1
- package/dist/db.d.ts +28 -8
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +91 -38
- package/dist/db.js.map +1 -1
- package/dist/errors.d.ts +11 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +13 -1
- package/dist/errors.js.map +1 -1
- package/dist/face.d.ts +19 -25
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +30 -24
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +13 -26
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +89 -116
- package/dist/fields.js.map +1 -1
- package/dist/immutable.d.ts +10 -0
- package/dist/immutable.d.ts.map +1 -0
- package/dist/immutable.js +75 -0
- package/dist/immutable.js.map +1 -0
- package/dist/index.d.ts +16 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/internal/log.d.ts +2 -0
- package/dist/internal/log.d.ts.map +1 -1
- package/dist/internal/log.js +2 -0
- package/dist/internal/log.js.map +1 -1
- package/dist/law.d.ts +13 -11
- package/dist/law.d.ts.map +1 -1
- package/dist/law.js +1 -1
- package/dist/law.js.map +1 -1
- package/dist/lower.d.ts.map +1 -1
- package/dist/lower.js +11 -6
- package/dist/lower.js.map +1 -1
- package/dist/native.d.ts +1 -1
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/atom.d.ts +1 -1
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js.map +1 -1
- package/dist/query/description.d.ts +33 -0
- package/dist/query/description.d.ts.map +1 -0
- package/dist/query/description.js +302 -0
- package/dist/query/description.js.map +1 -0
- package/dist/query/find.d.ts +21 -6
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts +75 -10
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +132 -163
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts +7 -2
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +240 -53
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +8 -10
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +24 -16
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +5 -10
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +22 -48
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +30 -82
- package/dist/relation.js.map +1 -1
- package/dist/rows.d.ts +7 -4
- package/dist/rows.d.ts.map +1 -1
- package/dist/rows.js +44 -198
- package/dist/rows.js.map +1 -1
- package/dist/runtime-errors.d.ts +26 -2
- package/dist/runtime-errors.d.ts.map +1 -1
- package/dist/runtime-errors.js +43 -3
- package/dist/runtime-errors.js.map +1 -1
- package/dist/schema.d.ts +9 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +85 -24
- package/dist/schema.js.map +1 -1
- package/dist/selection.d.ts +27 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/selection.js +104 -0
- package/dist/selection.js.map +1 -0
- package/dist/shape.d.ts +12 -12
- package/dist/shape.d.ts.map +1 -1
- package/dist/statements.d.ts +17 -52
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +92 -111
- package/dist/statements.js.map +1 -1
- package/dist/values.d.ts +25 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +203 -0
- package/dist/values.js.map +1 -0
- package/pack-provenance.json +3 -3
- package/package.json +4 -4
- package/src/capacity.ts +93 -80
- package/src/changes.ts +183 -54
- package/src/closed.ts +161 -314
- package/src/codec.ts +116 -190
- package/src/compile.ts +56 -32
- package/src/db-native.ts +42 -0
- package/src/db.ts +134 -52
- package/src/errors.ts +16 -1
- package/src/face.ts +44 -74
- package/src/fields.ts +100 -146
- package/src/immutable.ts +76 -0
- package/src/index.ts +36 -29
- package/src/internal/log.ts +2 -0
- package/src/law.ts +21 -18
- package/src/lower.ts +13 -10
- package/src/native.ts +2 -0
- package/src/query/atom.ts +1 -0
- package/src/query/description.ts +394 -0
- package/src/query/find.ts +20 -7
- package/src/query/lower.ts +177 -187
- package/src/query/parse-ir.ts +247 -54
- package/src/query/run.ts +15 -9
- package/src/query/scope.ts +42 -33
- package/src/relation.ts +58 -142
- package/src/rows.ts +62 -205
- package/src/runtime-errors.ts +47 -3
- package/src/schema.ts +99 -29
- package/src/selection.ts +124 -0
- package/src/shape.ts +18 -12
- package/src/statements.ts +138 -189
- package/src/values.ts +223 -0
package/src/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util"
|
|
1
2
|
import { AuthoringError, SdkInvariantError } from "#errors.ts"
|
|
2
3
|
/**
|
|
3
4
|
* `schema` — assembles relations and statements into a theory value (the
|
|
@@ -17,13 +18,15 @@ import { AuthoringError, SdkInvariantError } from "#errors.ts"
|
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import type { AnyClosed } from "#closed.ts"
|
|
20
|
-
import { isClosedMember, sealedFieldOf } from "#closed.ts"
|
|
21
|
-
import type {
|
|
21
|
+
import { isClosedMember, memberDescriptor, membersAgree, sealedFieldOf } from "#closed.ts"
|
|
22
|
+
import type { AnyFace } from "#face.ts"
|
|
22
23
|
import { assertDeclarationOrderKey, assertDeclarationRecord, rosterOf } from "#fields.ts"
|
|
24
|
+
import { descriptorCache, isImmutable } from "#immutable.ts"
|
|
23
25
|
import { type ClassesOf, classesComplete, computeClasses, type LawfulStatements, type SchemaClasses } from "#law.ts"
|
|
24
26
|
import type { AnyRelation } from "#relation.ts"
|
|
25
27
|
import type { LiteralSetSpec, LiteralSpec } from "#spec.ts"
|
|
26
|
-
import {
|
|
28
|
+
import { renderStatement, type Statement, statementDescriptor } from "#statements.ts"
|
|
29
|
+
import { arrayValue, recordValue } from "#values.ts"
|
|
27
30
|
|
|
28
31
|
interface ImpliedKeys {
|
|
29
32
|
readonly rendered: ReadonlySet<string>
|
|
@@ -54,7 +57,7 @@ function collectImplied(name: string, relations: SchemaRelations): ImpliedKeys {
|
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
function statementOwners(statement: Statement): readonly SchemaRelation[] {
|
|
57
|
-
const data = statement
|
|
60
|
+
const data = statement
|
|
58
61
|
if (data.kind === "key") {
|
|
59
62
|
return [data.owner]
|
|
60
63
|
}
|
|
@@ -69,9 +72,9 @@ function verifyMembership(name: string, relations: SchemaRelations, statement: S
|
|
|
69
72
|
message: `schema ${name}: relation ${owner.name} is not declared in this schema — ${rendered}`
|
|
70
73
|
})
|
|
71
74
|
}
|
|
72
|
-
if (member
|
|
75
|
+
if (!membersAgree(member, owner)) {
|
|
73
76
|
throw new AuthoringError({
|
|
74
|
-
message: `schema ${name}: statement references a different relation
|
|
77
|
+
message: `schema ${name}: statement references a different relation declaration named ${owner.name} than the one this schema declares — ${rendered}`
|
|
75
78
|
})
|
|
76
79
|
}
|
|
77
80
|
}
|
|
@@ -86,7 +89,7 @@ function bindingLiterals(set: LiteralSetSpec): readonly LiteralSpec[] {
|
|
|
86
89
|
|
|
87
90
|
function verifyBindingHandles(
|
|
88
91
|
name: string,
|
|
89
|
-
face:
|
|
92
|
+
face: AnyFace,
|
|
90
93
|
binding: { readonly field: string; readonly set: LiteralSetSpec },
|
|
91
94
|
rendered: string
|
|
92
95
|
): void {
|
|
@@ -109,7 +112,7 @@ function verifyBindingHandles(
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
function verifyHandles(name: string, statement: Statement, rendered: string): void {
|
|
112
|
-
const data = statement
|
|
115
|
+
const data = statement
|
|
113
116
|
if (data.kind === "key") {
|
|
114
117
|
return
|
|
115
118
|
}
|
|
@@ -122,11 +125,11 @@ function verifyHandles(name: string, statement: Statement, rendered: string): vo
|
|
|
122
125
|
|
|
123
126
|
function closedTargetOf(statements: readonly Statement[], owner: string, field: string): string | undefined {
|
|
124
127
|
for (const statement of statements) {
|
|
125
|
-
const data = statement
|
|
128
|
+
const data = statement
|
|
126
129
|
if (data.kind !== "containment" && data.kind !== "mirrors") {
|
|
127
130
|
continue
|
|
128
131
|
}
|
|
129
|
-
const pairs: Array<readonly [
|
|
132
|
+
const pairs: Array<readonly [AnyFace, AnyFace]> = [[data.source, data.target]]
|
|
130
133
|
if (data.kind === "mirrors") {
|
|
131
134
|
pairs.push([data.target, data.source])
|
|
132
135
|
}
|
|
@@ -148,7 +151,7 @@ function closedTargetOf(statements: readonly Statement[], owner: string, field:
|
|
|
148
151
|
|
|
149
152
|
function verifyClosedReferences(name: string, statements: readonly Statement[]): void {
|
|
150
153
|
for (const statement of statements) {
|
|
151
|
-
const data = statement
|
|
154
|
+
const data = statement
|
|
152
155
|
if (data.kind === "key") {
|
|
153
156
|
continue
|
|
154
157
|
}
|
|
@@ -164,7 +167,7 @@ function verifyClosedReferences(name: string, statements: readonly Statement[]):
|
|
|
164
167
|
function verifyClosedReferenceBinding(
|
|
165
168
|
name: string,
|
|
166
169
|
statements: readonly Statement[],
|
|
167
|
-
face:
|
|
170
|
+
face: AnyFace,
|
|
168
171
|
binding: { readonly field: string; readonly set: LiteralSetSpec },
|
|
169
172
|
rendered: string
|
|
170
173
|
): void {
|
|
@@ -213,7 +216,7 @@ function verifyTargetKeys(
|
|
|
213
216
|
): void {
|
|
214
217
|
const declared = new Map<string, Array<readonly string[]>>()
|
|
215
218
|
for (const statement of statements) {
|
|
216
|
-
const data = statement
|
|
219
|
+
const data = statement
|
|
217
220
|
if (data.kind !== "key") {
|
|
218
221
|
continue
|
|
219
222
|
}
|
|
@@ -225,7 +228,7 @@ function verifyTargetKeys(
|
|
|
225
228
|
}
|
|
226
229
|
}
|
|
227
230
|
for (const statement of statements) {
|
|
228
|
-
const data = statement
|
|
231
|
+
const data = statement
|
|
229
232
|
if (data.kind === "key") {
|
|
230
233
|
continue
|
|
231
234
|
}
|
|
@@ -251,7 +254,7 @@ function verifyTargetKeys(
|
|
|
251
254
|
*/
|
|
252
255
|
function verifyTargetKeyFace(
|
|
253
256
|
name: string,
|
|
254
|
-
face:
|
|
257
|
+
face: AnyFace,
|
|
255
258
|
implied: ReadonlyMap<string, ReadonlyArray<readonly string[]>>,
|
|
256
259
|
declared: ReadonlyMap<string, ReadonlyArray<readonly string[]>>,
|
|
257
260
|
rendered: string
|
|
@@ -310,25 +313,92 @@ interface Schema<Rels extends SchemaRelations, Classes extends SchemaClasses = S
|
|
|
310
313
|
|
|
311
314
|
type AnySchema = Schema<SchemaRelations>
|
|
312
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Logical schema equivalence, including the ordered native ordinals and
|
|
318
|
+
* host decoding domains. Schema display names do not change the theory.
|
|
319
|
+
* A database/snapshot still has its own resource identity and lifetime.
|
|
320
|
+
*/
|
|
321
|
+
function schemasAgree(leftInput: AnySchema, rightInput: AnySchema): boolean {
|
|
322
|
+
const left = schemaDescriptor(leftInput)
|
|
323
|
+
const right = schemaDescriptor(rightInput)
|
|
324
|
+
if (left === right) return true
|
|
325
|
+
const cacheable = isImmutable(left) && isImmutable(right)
|
|
326
|
+
const cached = schemaComparisons.get(left)?.get(right)
|
|
327
|
+
if (cacheable && cached !== undefined) return cached
|
|
328
|
+
const names = Object.keys(left.relations)
|
|
329
|
+
const otherNames = Object.keys(right.relations)
|
|
330
|
+
const equal =
|
|
331
|
+
names.length === otherNames.length &&
|
|
332
|
+
names.every((name, index) => {
|
|
333
|
+
const member = right.relations[name]
|
|
334
|
+
return name === otherNames[index] && member !== undefined && membersAgree(left.relations[name], member)
|
|
335
|
+
}) &&
|
|
336
|
+
isDeepStrictEqual(left.statements, right.statements) &&
|
|
337
|
+
isDeepStrictEqual(left.classes, right.classes)
|
|
338
|
+
if (cacheable) {
|
|
339
|
+
let comparisons = schemaComparisons.get(left)
|
|
340
|
+
if (comparisons === undefined) {
|
|
341
|
+
comparisons = new WeakMap()
|
|
342
|
+
schemaComparisons.set(left, comparisons)
|
|
343
|
+
}
|
|
344
|
+
comparisons.set(right, equal)
|
|
345
|
+
}
|
|
346
|
+
return equal
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const schemaComparisons = new WeakMap<AnySchema, WeakMap<AnySchema, boolean>>()
|
|
350
|
+
|
|
351
|
+
function schemaDescriptor<S extends AnySchema>(input: S): S
|
|
352
|
+
function schemaDescriptor(input: unknown): AnySchema
|
|
353
|
+
function schemaDescriptor(input: unknown): AnySchema {
|
|
354
|
+
return checkedSchema(input)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const checkedSchema = descriptorCache((input): AnySchema => {
|
|
358
|
+
const raw = recordValue("schema", input, ["name", "relations", "statements", "classes"])
|
|
359
|
+
if (typeof raw.name !== "string") throw new AuthoringError({ message: "schema: expected a name" })
|
|
360
|
+
const relations = ownRelations(raw.name, raw.relations)
|
|
361
|
+
const statements = arrayValue("schema statements", raw.statements, (_, statement) => statementDescriptor(statement))
|
|
362
|
+
const result = schema(raw.name, relations, statements)
|
|
363
|
+
const classes = recordValue("schema classes", raw.classes, Object.keys(result.classes))
|
|
364
|
+
for (const [name, fields] of Object.entries(result.classes)) {
|
|
365
|
+
const supplied = recordValue(`schema classes.${name}`, classes[name], Object.keys(fields))
|
|
366
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
367
|
+
if (supplied[field] !== value)
|
|
368
|
+
throw new AuthoringError({ message: `schema classes.${name}.${field}: incorrect class` })
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return result
|
|
372
|
+
})
|
|
373
|
+
|
|
313
374
|
type EvaluatedClasses<C extends SchemaClasses> = C extends SchemaClasses
|
|
314
375
|
? { readonly [N in keyof C]: { readonly [F in keyof C[N]]: C[N][F] } }
|
|
315
376
|
: never
|
|
316
377
|
|
|
378
|
+
function ownRelations<R extends SchemaRelations>(name: string, input: R): R
|
|
379
|
+
function ownRelations(name: string, input: unknown): SchemaRelations
|
|
380
|
+
function ownRelations(name: string, input: unknown): SchemaRelations {
|
|
381
|
+
if (typeof input !== "object" || input === null)
|
|
382
|
+
throw new AuthoringError({ message: `schema ${name}: expected relations` })
|
|
383
|
+
assertDeclarationRecord(`schema ${name} relations`, input)
|
|
384
|
+
return Object.freeze(
|
|
385
|
+
Object.fromEntries(Object.entries(input).map(([name, member]) => [name, memberDescriptor(member)]))
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
317
389
|
function schema<const Rels extends SchemaRelations, const Stmts extends readonly Statement[]>(
|
|
318
390
|
name: string,
|
|
319
391
|
relations: Rels,
|
|
320
392
|
statements: Stmts & LawfulStatements<Rels, Stmts>
|
|
321
393
|
): Schema<Rels, EvaluatedClasses<ClassesOf<Rels, Stmts>>> {
|
|
322
|
-
|
|
394
|
+
assertDeclarationOrderKey("schema", name)
|
|
395
|
+
const ownedRelations = ownRelations(name, relations)
|
|
396
|
+
const ownedStatements = arrayValue("schema statements", statements, (_, statement) => statementDescriptor(statement))
|
|
397
|
+
const implied = collectImplied(name, ownedRelations)
|
|
323
398
|
const seen = new Set<string>()
|
|
324
|
-
for (const statement of
|
|
325
|
-
if (!isStatement(statement)) {
|
|
326
|
-
throw new AuthoringError({
|
|
327
|
-
message: `schema ${name}: a statement is minted only by key/contained/mirrors/capacity — a structural literal skips the construction-time arity and roster walls`
|
|
328
|
-
})
|
|
329
|
-
}
|
|
399
|
+
for (const statement of ownedStatements) {
|
|
330
400
|
const rendered = renderStatement(statement)
|
|
331
|
-
verifyMembership(name,
|
|
401
|
+
verifyMembership(name, ownedRelations, statement, rendered)
|
|
332
402
|
if (implied.rendered.has(rendered)) {
|
|
333
403
|
throw new AuthoringError({
|
|
334
404
|
message: `schema ${name}: ${rendered} is redundant here (closedness already implies it) — and rejected as a duplicate`
|
|
@@ -340,14 +410,14 @@ function schema<const Rels extends SchemaRelations, const Stmts extends readonly
|
|
|
340
410
|
seen.add(rendered)
|
|
341
411
|
verifyHandles(name, statement, rendered)
|
|
342
412
|
}
|
|
343
|
-
verifyClosedReferences(name,
|
|
344
|
-
verifyTargetKeys(name,
|
|
345
|
-
const classes = computeClasses(name,
|
|
346
|
-
if (!classesComplete<EvaluatedClasses<ClassesOf<Rels, Stmts>>>(classes,
|
|
413
|
+
verifyClosedReferences(name, ownedStatements)
|
|
414
|
+
verifyTargetKeys(name, ownedStatements, implied.roster)
|
|
415
|
+
const classes = computeClasses(name, ownedRelations, ownedStatements)
|
|
416
|
+
if (!classesComplete<EvaluatedClasses<ClassesOf<Rels, Stmts>>>(classes, ownedRelations)) {
|
|
347
417
|
throw new SdkInvariantError({ message: `schema ${name}: class-map construction incomplete` })
|
|
348
418
|
}
|
|
349
|
-
return Object.freeze({ name, relations, statements: Object.freeze(
|
|
419
|
+
return Object.freeze({ name, relations: ownedRelations, statements: Object.freeze(ownedStatements), classes })
|
|
350
420
|
}
|
|
351
421
|
|
|
352
422
|
export type { AnySchema, Schema, SchemaRelation, SchemaRelations }
|
|
353
|
-
export { schema }
|
|
423
|
+
export { schema, schemaDescriptor, schemasAgree }
|
package/src/selection.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type AnyClosed, memberDescriptor, sealedFieldOf, sealedFieldsOf } from "#closed.ts"
|
|
2
|
+
import { AuthoringError } from "#errors.ts"
|
|
3
|
+
import { type AnyField, assertDeclarationRecord, type ClosedIdField, type Infer, literalOf } from "#fields.ts"
|
|
4
|
+
import type { AnyRelation, RelationFields } from "#relation.ts"
|
|
5
|
+
import { type LiteralSetSpec, type LiteralSpec, renderLiteral } from "#spec.ts"
|
|
6
|
+
import { arrayValue, recordValue } from "#values.ts"
|
|
7
|
+
|
|
8
|
+
type Selectable = AnyRelation | AnyClosed
|
|
9
|
+
type FieldsOf<R extends Selectable> = R extends AnyRelation
|
|
10
|
+
? RelationFields<R>
|
|
11
|
+
: R extends AnyClosed
|
|
12
|
+
? { readonly id: ClosedIdField<R["name"], R["handles"]> } & R["columns"]
|
|
13
|
+
: never
|
|
14
|
+
type SelectionInput<R extends Selectable> = {
|
|
15
|
+
readonly [K in keyof FieldsOf<R>]?: Infer<FieldsOf<R>[K]> | readonly Infer<FieldsOf<R>[K]>[]
|
|
16
|
+
}
|
|
17
|
+
interface SelectionBinding {
|
|
18
|
+
readonly field: string
|
|
19
|
+
readonly set: LiteralSetSpec
|
|
20
|
+
}
|
|
21
|
+
interface Selected<R extends Selectable = Selectable> {
|
|
22
|
+
readonly relation: R
|
|
23
|
+
readonly selection: readonly SelectionBinding[]
|
|
24
|
+
}
|
|
25
|
+
type AnySelected = Selected
|
|
26
|
+
|
|
27
|
+
/** Checked logical literals use the same field interpreter as fact values. */
|
|
28
|
+
function selectionLiteral(field: AnyField, input: unknown): LiteralSpec {
|
|
29
|
+
const raw = recordValue("selection literal", input, "closed" in field ? ["kind", "handle"] : ["kind", "value"])
|
|
30
|
+
if ("closed" in field) {
|
|
31
|
+
if (raw.kind !== "handle") throw new AuthoringError({ message: "closed selection requires a handle literal" })
|
|
32
|
+
return Object.freeze(literalOf(field, raw.handle))
|
|
33
|
+
}
|
|
34
|
+
if (raw.kind !== "value") throw new AuthoringError({ message: "selection requires a value literal" })
|
|
35
|
+
const value = recordValue(
|
|
36
|
+
"selection value",
|
|
37
|
+
raw.value,
|
|
38
|
+
field.kind === "interval" ? ["kind", "start", "end"] : ["kind", "value"]
|
|
39
|
+
)
|
|
40
|
+
let kind: string = field.kind
|
|
41
|
+
if (field.kind === "str") kind = "string"
|
|
42
|
+
if (field.kind === "bytes") kind = "fixedBytes"
|
|
43
|
+
if (field.kind === "interval") kind = { u64: "intervalU64", i64: "intervalI64", f64: "intervalF64" }[field.element]
|
|
44
|
+
if (value.kind !== kind) throw new AuthoringError({ message: `selection requires the ${kind} value tag` })
|
|
45
|
+
const literal = literalOf(field, field.kind === "interval" ? { start: value.start, end: value.end } : value.value)
|
|
46
|
+
if (literal.kind === "value") Object.freeze(literal.value)
|
|
47
|
+
return Object.freeze(literal)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selectionBindings(owner: Selectable, input: unknown): readonly SelectionBinding[] {
|
|
51
|
+
if (!Array.isArray(input)) throw new AuthoringError({ message: "selection: expected bindings" })
|
|
52
|
+
const selected = new Set<string>()
|
|
53
|
+
return Object.freeze(
|
|
54
|
+
arrayValue("selection", input, (_, raw) => {
|
|
55
|
+
const binding = recordValue("selection binding", raw, ["field", "set"])
|
|
56
|
+
if (typeof binding.field !== "string") throw new AuthoringError({ message: "selection: expected a field name" })
|
|
57
|
+
const field = sealedFieldOf(owner, binding.field)
|
|
58
|
+
if (field === undefined)
|
|
59
|
+
throw new AuthoringError({ message: `relation ${owner.name} has no field ${binding.field}` })
|
|
60
|
+
if (selected.has(binding.field)) throw new AuthoringError({ message: "selection: duplicate field" })
|
|
61
|
+
selected.add(binding.field)
|
|
62
|
+
const set = binding.set
|
|
63
|
+
if (typeof set !== "object" || set === null || !("kind" in set))
|
|
64
|
+
throw new AuthoringError({ message: "literal set: expected a descriptor" })
|
|
65
|
+
assertDeclarationRecord("literal set", set)
|
|
66
|
+
if (set.kind === "one") {
|
|
67
|
+
const rawSet = recordValue("literal set", set, ["kind", "literal"])
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
field: binding.field,
|
|
70
|
+
set: Object.freeze({ kind: "one" as const, literal: selectionLiteral(field, rawSet.literal) })
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
if (set.kind !== "many") throw new AuthoringError({ message: "literal set: unknown kind" })
|
|
74
|
+
const rawSet = recordValue("literal set", set, ["kind", "literals"])
|
|
75
|
+
if (!Array.isArray(rawSet.literals) || rawSet.literals.length < 2)
|
|
76
|
+
throw new AuthoringError({ message: "literal set: expected at least two distinct literals" })
|
|
77
|
+
const literals = arrayValue("selection literals", rawSet.literals, (_, literal) =>
|
|
78
|
+
selectionLiteral(field, literal)
|
|
79
|
+
)
|
|
80
|
+
if (new Set(literals.map(renderLiteral)).size !== literals.length)
|
|
81
|
+
throw new AuthoringError({ message: "literal set: duplicate literal" })
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
field: binding.field,
|
|
84
|
+
set: Object.freeze({ kind: "many" as const, literals: Object.freeze(literals) })
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function select<R extends Selectable>(relation: R, input: SelectionInput<R>): Selected<R> {
|
|
91
|
+
const owner = memberDescriptor(relation)
|
|
92
|
+
assertDeclarationRecord(`relation ${owner.name} selection`, input)
|
|
93
|
+
const fields = sealedFieldsOf(owner)
|
|
94
|
+
const selection: SelectionBinding[] = Object.entries(input).map(([name, entry]) => {
|
|
95
|
+
const field = fields.find((field) => field.name === name)?.field
|
|
96
|
+
if (field === undefined) throw new AuthoringError({ message: `relation ${owner.name} has no field ${name}` })
|
|
97
|
+
if (!Array.isArray(entry)) return { field: name, set: { kind: "one", literal: literalOf(field, entry) } }
|
|
98
|
+
const context = `relation ${owner.name}.${name}`
|
|
99
|
+
if (entry.length < 2)
|
|
100
|
+
throw new AuthoringError({
|
|
101
|
+
message:
|
|
102
|
+
entry.length === 0
|
|
103
|
+
? `${context}: an empty literal set selects nothing — write the selection you mean`
|
|
104
|
+
: `${context}: a one-element literal set is the bare literal respelled — write the literal`
|
|
105
|
+
})
|
|
106
|
+
const literals = arrayValue(context, entry, (_, value) => literalOf(field, value))
|
|
107
|
+
const seen = new Set<string>()
|
|
108
|
+
for (const literal of literals) {
|
|
109
|
+
const rendered = renderLiteral(literal)
|
|
110
|
+
if (seen.has(rendered))
|
|
111
|
+
throw new AuthoringError({ message: `${context}: the literal set spells ${rendered} twice — write it once` })
|
|
112
|
+
seen.add(rendered)
|
|
113
|
+
}
|
|
114
|
+
return { field: name, set: { kind: "many", literals } }
|
|
115
|
+
})
|
|
116
|
+
if (selection.length === 0)
|
|
117
|
+
throw new AuthoringError({
|
|
118
|
+
message: `relation ${owner.name}: an empty selection is the bare relation respelled — pass the relation itself`
|
|
119
|
+
})
|
|
120
|
+
return Object.freeze({ relation: owner, selection: selectionBindings(owner, selection) })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type { AnySelected, FieldsOf, Selectable, Selected, SelectionBinding, SelectionInput }
|
|
124
|
+
export { select, selectionBindings, selectionLiteral }
|
package/src/shape.ts
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import type { Query } from "#query/lower.ts"
|
|
2
|
-
import type { ParamsRecord } from "#query/scope.ts"
|
|
2
|
+
import type { inferred, ParamsRecord } from "#query/scope.ts"
|
|
3
3
|
/**
|
|
4
4
|
* Shared derived types: `S` is a
|
|
5
5
|
* declared core schema value's type, `Rel<S>` its ordinary (writable)
|
|
6
|
-
* relations, `Fact<R>` the inferred row object, `Key<
|
|
7
|
-
*
|
|
6
|
+
* relations, `Fact<R>` the inferred row object, `Key<K>` the exact value
|
|
7
|
+
* of a declared key accepted by `QueryReader.get`, and `QueryTemplate<S, P, A>` the
|
|
8
8
|
* immutable schema-bound logical query template. All are derived from the
|
|
9
9
|
* existing typed descriptors — no second hand-maintained roster.
|
|
10
10
|
*/
|
|
11
11
|
import type { AnyRelation, Fact } from "#relation.ts"
|
|
12
12
|
import type { AnySchema } from "#schema.ts"
|
|
13
|
+
import type { KeyStatement } from "#statements.ts"
|
|
13
14
|
|
|
14
15
|
/** The ordinary relations of a schema (closed vocabularies are ground axioms, never ingestion targets). */
|
|
15
16
|
type Rel<S extends AnySchema> = Extract<S["relations"][keyof S["relations"]], AnyRelation>
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
* otherwise the first declared `key`). The exact projection is a schema
|
|
21
|
-
* fact, verified at execution against the compiled tables; the type admits
|
|
22
|
-
* any subset of the fact's own fields and the runtime refuses a mismatch
|
|
23
|
-
* with a typed `DbError` before any native work.
|
|
19
|
+
* The structural value of one declared key, derived from its owner's
|
|
20
|
+
* fields and its exact projection. Every selected field is required.
|
|
24
21
|
*/
|
|
25
|
-
type Key<
|
|
22
|
+
type Key<K extends KeyStatement<AnyRelation, readonly string[]>> = Pick<
|
|
23
|
+
Fact<K["owner"]>,
|
|
24
|
+
Extract<K["projection"][number], keyof Fact<K["owner"]>>
|
|
25
|
+
>
|
|
26
26
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** The immutable executable view of a query. Readers consume its schema/data
|
|
28
|
+
* and inferred result/parameters, not the author's rule-building methods.
|
|
29
|
+
* Runtime schema identity remains authoritative for dynamically built schemas.
|
|
30
|
+
*/
|
|
31
|
+
type QueryTemplate<S extends AnySchema, P extends ParamsRecord, A> = Pick<
|
|
32
|
+
Query<S["relations"], A, P>,
|
|
33
|
+
"schema" | "data" | typeof inferred
|
|
34
|
+
>
|
|
29
35
|
|
|
30
36
|
export type { Key, QueryTemplate, Rel }
|