@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.
Files changed (152) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +22 -59
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +66 -13
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +26 -3
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +89 -44
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +4 -9
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +108 -183
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +16 -16
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +36 -15
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +39 -0
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +28 -8
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +91 -38
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +2 -0
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +2 -0
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +13 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/native.d.ts +1 -1
  63. package/dist/native.d.ts.map +1 -1
  64. package/dist/native.js.map +1 -1
  65. package/dist/query/atom.d.ts +1 -1
  66. package/dist/query/atom.d.ts.map +1 -1
  67. package/dist/query/atom.js.map +1 -1
  68. package/dist/query/description.d.ts +33 -0
  69. package/dist/query/description.d.ts.map +1 -0
  70. package/dist/query/description.js +302 -0
  71. package/dist/query/description.js.map +1 -0
  72. package/dist/query/find.d.ts +21 -6
  73. package/dist/query/find.d.ts.map +1 -1
  74. package/dist/query/find.js.map +1 -1
  75. package/dist/query/lower.d.ts +75 -10
  76. package/dist/query/lower.d.ts.map +1 -1
  77. package/dist/query/lower.js +132 -163
  78. package/dist/query/lower.js.map +1 -1
  79. package/dist/query/parse-ir.d.ts +7 -2
  80. package/dist/query/parse-ir.d.ts.map +1 -1
  81. package/dist/query/parse-ir.js +240 -53
  82. package/dist/query/parse-ir.js.map +1 -1
  83. package/dist/query/run.d.ts.map +1 -1
  84. package/dist/query/run.js +8 -10
  85. package/dist/query/run.js.map +1 -1
  86. package/dist/query/scope.d.ts +24 -16
  87. package/dist/query/scope.d.ts.map +1 -1
  88. package/dist/query/scope.js +5 -10
  89. package/dist/query/scope.js.map +1 -1
  90. package/dist/relation.d.ts +22 -48
  91. package/dist/relation.d.ts.map +1 -1
  92. package/dist/relation.js +30 -82
  93. package/dist/relation.js.map +1 -1
  94. package/dist/rows.d.ts +7 -4
  95. package/dist/rows.d.ts.map +1 -1
  96. package/dist/rows.js +44 -198
  97. package/dist/rows.js.map +1 -1
  98. package/dist/runtime-errors.d.ts +26 -2
  99. package/dist/runtime-errors.d.ts.map +1 -1
  100. package/dist/runtime-errors.js +43 -3
  101. package/dist/runtime-errors.js.map +1 -1
  102. package/dist/schema.d.ts +9 -1
  103. package/dist/schema.d.ts.map +1 -1
  104. package/dist/schema.js +85 -24
  105. package/dist/schema.js.map +1 -1
  106. package/dist/selection.d.ts +27 -0
  107. package/dist/selection.d.ts.map +1 -0
  108. package/dist/selection.js +104 -0
  109. package/dist/selection.js.map +1 -0
  110. package/dist/shape.d.ts +12 -12
  111. package/dist/shape.d.ts.map +1 -1
  112. package/dist/statements.d.ts +17 -52
  113. package/dist/statements.d.ts.map +1 -1
  114. package/dist/statements.js +92 -111
  115. package/dist/statements.js.map +1 -1
  116. package/dist/values.d.ts +25 -0
  117. package/dist/values.d.ts.map +1 -0
  118. package/dist/values.js +203 -0
  119. package/dist/values.js.map +1 -0
  120. package/pack-provenance.json +3 -3
  121. package/package.json +4 -4
  122. package/src/capacity.ts +93 -80
  123. package/src/changes.ts +183 -54
  124. package/src/closed.ts +161 -314
  125. package/src/codec.ts +116 -190
  126. package/src/compile.ts +56 -32
  127. package/src/db-native.ts +42 -0
  128. package/src/db.ts +134 -52
  129. package/src/errors.ts +16 -1
  130. package/src/face.ts +44 -74
  131. package/src/fields.ts +100 -146
  132. package/src/immutable.ts +76 -0
  133. package/src/index.ts +36 -29
  134. package/src/internal/log.ts +2 -0
  135. package/src/law.ts +21 -18
  136. package/src/lower.ts +13 -10
  137. package/src/native.ts +2 -0
  138. package/src/query/atom.ts +1 -0
  139. package/src/query/description.ts +394 -0
  140. package/src/query/find.ts +20 -7
  141. package/src/query/lower.ts +177 -187
  142. package/src/query/parse-ir.ts +247 -54
  143. package/src/query/run.ts +15 -9
  144. package/src/query/scope.ts +42 -33
  145. package/src/relation.ts +58 -142
  146. package/src/rows.ts +62 -205
  147. package/src/runtime-errors.ts +47 -3
  148. package/src/schema.ts +99 -29
  149. package/src/selection.ts +124 -0
  150. package/src/shape.ts +18 -12
  151. package/src/statements.ts +138 -189
  152. 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 { FaceData } from "#face.ts"
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 { isStatement, renderStatement, type Statement } from "#statements.ts"
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.data
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 !== owner) {
75
+ if (!membersAgree(member, owner)) {
73
76
  throw new AuthoringError({
74
- message: `schema ${name}: statement references a different relation value named ${owner.name} than the one this schema declares — ${rendered}`
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: FaceData,
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.data
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.data
128
+ const data = statement
126
129
  if (data.kind !== "containment" && data.kind !== "mirrors") {
127
130
  continue
128
131
  }
129
- const pairs: Array<readonly [FaceData, FaceData]> = [[data.source, data.target]]
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.data
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: FaceData,
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.data
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.data
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: FaceData,
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
- const implied = collectImplied(name, relations)
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 statements) {
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, relations, statement, rendered)
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, statements)
344
- verifyTargetKeys(name, statements, implied.roster)
345
- const classes = computeClasses(name, relations, statements)
346
- if (!classesComplete<EvaluatedClasses<ClassesOf<Rels, Stmts>>>(classes, relations)) {
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([...statements]), classes })
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 }
@@ -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<R>` the primary-key
7
- * object accepted by `QueryReader.get`, and `QueryTemplate<S, P, A>` the
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 primary-key object of a relation: the fields of its FIRST
19
- * materialized key statement (a closed relation's synthetic `(id)`,
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<R extends AnyRelation> = Partial<Fact<R>>
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
- /** Immutable typed query template. */
28
- type QueryTemplate<S extends AnySchema, P extends ParamsRecord, A> = Query<S["relations"], A, P>
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 }