@bjornpagen/bumbledb 1.1.0 → 1.2.0

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 (148) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  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 +14 -10
  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 +10 -10
  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/lower.d.ts +60 -4
  73. package/dist/query/lower.d.ts.map +1 -1
  74. package/dist/query/lower.js +126 -161
  75. package/dist/query/lower.js.map +1 -1
  76. package/dist/query/parse-ir.d.ts +7 -2
  77. package/dist/query/parse-ir.d.ts.map +1 -1
  78. package/dist/query/parse-ir.js +240 -53
  79. package/dist/query/parse-ir.js.map +1 -1
  80. package/dist/query/run.d.ts.map +1 -1
  81. package/dist/query/run.js +8 -10
  82. package/dist/query/run.js.map +1 -1
  83. package/dist/query/scope.d.ts +3 -4
  84. package/dist/query/scope.d.ts.map +1 -1
  85. package/dist/query/scope.js +5 -10
  86. package/dist/query/scope.js.map +1 -1
  87. package/dist/relation.d.ts +22 -48
  88. package/dist/relation.d.ts.map +1 -1
  89. package/dist/relation.js +30 -82
  90. package/dist/relation.js.map +1 -1
  91. package/dist/rows.d.ts +7 -4
  92. package/dist/rows.d.ts.map +1 -1
  93. package/dist/rows.js +44 -198
  94. package/dist/rows.js.map +1 -1
  95. package/dist/runtime-errors.d.ts +26 -2
  96. package/dist/runtime-errors.d.ts.map +1 -1
  97. package/dist/runtime-errors.js +43 -3
  98. package/dist/runtime-errors.js.map +1 -1
  99. package/dist/schema.d.ts +9 -1
  100. package/dist/schema.d.ts.map +1 -1
  101. package/dist/schema.js +85 -24
  102. package/dist/schema.js.map +1 -1
  103. package/dist/selection.d.ts +27 -0
  104. package/dist/selection.d.ts.map +1 -0
  105. package/dist/selection.js +104 -0
  106. package/dist/selection.js.map +1 -0
  107. package/dist/shape.d.ts +6 -9
  108. package/dist/shape.d.ts.map +1 -1
  109. package/dist/statements.d.ts +17 -52
  110. package/dist/statements.d.ts.map +1 -1
  111. package/dist/statements.js +92 -111
  112. package/dist/statements.js.map +1 -1
  113. package/dist/values.d.ts +25 -0
  114. package/dist/values.d.ts.map +1 -0
  115. package/dist/values.js +203 -0
  116. package/dist/values.js.map +1 -0
  117. package/pack-provenance.json +3 -3
  118. package/package.json +4 -4
  119. package/src/capacity.ts +86 -76
  120. package/src/changes.ts +183 -54
  121. package/src/closed.ts +161 -314
  122. package/src/codec.ts +116 -190
  123. package/src/compile.ts +56 -32
  124. package/src/db-native.ts +42 -0
  125. package/src/db.ts +134 -52
  126. package/src/errors.ts +16 -1
  127. package/src/face.ts +44 -74
  128. package/src/fields.ts +100 -146
  129. package/src/immutable.ts +76 -0
  130. package/src/index.ts +34 -28
  131. package/src/internal/log.ts +2 -0
  132. package/src/law.ts +13 -13
  133. package/src/lower.ts +13 -10
  134. package/src/native.ts +2 -0
  135. package/src/query/atom.ts +1 -0
  136. package/src/query/description.ts +394 -0
  137. package/src/query/lower.ts +139 -173
  138. package/src/query/parse-ir.ts +247 -54
  139. package/src/query/run.ts +15 -9
  140. package/src/query/scope.ts +10 -18
  141. package/src/relation.ts +58 -142
  142. package/src/rows.ts +62 -205
  143. package/src/runtime-errors.ts +47 -3
  144. package/src/schema.ts +99 -29
  145. package/src/selection.ts +124 -0
  146. package/src/shape.ts +9 -9
  147. package/src/statements.ts +138 -189
  148. package/src/values.ts +223 -0
package/src/rows.ts CHANGED
@@ -17,9 +17,9 @@ import { AuthoringError, SdkInvariantError } from "#errors.ts"
17
17
  * scans and byte copies.
18
18
  */
19
19
  import type { AnyClosedRoster, AnyField } from "#fields.ts"
20
- import { isFloatIntervalValue, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
21
- import type { AnyRelation, Fact, RelationData } from "#relation.ts"
22
- import { Uuid } from "#uuid.ts"
20
+ import { literalShapeError, rosterOf } from "#fields.ts"
21
+ import { type AnyRelation, type Fact, relationFields } from "#relation.ts"
22
+ import { fieldValue, recordValue } from "#values.ts"
23
23
 
24
24
  /**
25
25
  * One owned cell at the private bridge boundary. The declared sealed field
@@ -44,20 +44,12 @@ interface FlatRows {
44
44
  }
45
45
 
46
46
  function recordOf(fact: object): Readonly<Record<string, unknown>> {
47
- if (typeof fact !== "object" && typeof fact !== "function") {
48
- throw new AuthoringError({ message: "fact object is not string-indexable" })
47
+ if (typeof fact !== "object" || fact === null) {
48
+ throw new AuthoringError({ message: "expected a fact record" })
49
49
  }
50
50
  return fact as Readonly<Record<string, unknown>>
51
51
  }
52
52
 
53
- function refuseShared(context: string, value: Uint8Array): void {
54
- if (!(value.buffer instanceof ArrayBuffer)) {
55
- throw new AuthoringError({
56
- message: `${context}: SharedArrayBuffer-backed views are refused — make a synchronized stable copy into ordinary unshared input first`
57
- })
58
- }
59
- }
60
-
61
53
  /**
62
54
  * Approximate payload size for host batching, not a memory allowance.
63
55
  * Strings use two bytes per UTF-16 code unit, not their UTF-8 wire size.
@@ -81,26 +73,11 @@ function hostCellCharge(value: unknown): bigint {
81
73
  return 0n
82
74
  }
83
75
 
84
- /**
85
- * The write half of the closed bijection: one handle NAME to its u64 row
86
- * id (declaration order = row ids). An unknown name is a pointed refusal
87
- * naming the vocabulary and its roster.
88
- */
89
- function closedCellOf(context: string, closed: AnyClosedRoster, name: string): CellValue {
90
- const id = closed.handles.indexOf(name)
91
- if (id === -1) {
92
- throw new AuthoringError({
93
- message: `${context}: "${name}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
94
- })
95
- }
96
- return BigInt(id)
97
- }
98
-
99
76
  function handleOf(context: string, closed: AnyClosedRoster, cell: unknown): string {
100
77
  if (typeof cell !== "bigint") {
101
78
  throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
102
79
  }
103
- const handle = closed.handles[Number(cell)]
80
+ const handle = cell < 0n || cell >= BigInt(closed.handles.length) ? undefined : closed.handles[Number(cell)]
104
81
  if (handle === undefined) {
105
82
  throw new AuthoringError({
106
83
  message: `${context}: id ${cell} is outside the ${closed.name} roster (${closed.handles.join(", ")})`
@@ -110,86 +87,21 @@ function handleOf(context: string, closed: AnyClosedRoster, cell: unknown): stri
110
87
  }
111
88
 
112
89
  function cellOf(context: string, field: AnyField, value: unknown): CellValue {
113
- const roster = rosterOf(field)
114
- if (roster !== undefined) {
115
- if (typeof value !== "string") {
116
- throw literalShapeError(context, `a ${roster.name} handle name (string)`, value)
117
- }
118
- return closedCellOf(context, roster, value)
119
- }
120
- switch (field.kind) {
121
- case "bool": {
122
- if (typeof value !== "boolean") {
123
- throw literalShapeError(context, "boolean", value)
124
- }
125
- return value
126
- }
127
- case "u64":
128
- case "i64": {
129
- if (typeof value !== "bigint") {
130
- throw literalShapeError(context, "bigint", value)
131
- }
132
- return value
133
- }
134
- case "str": {
135
- if (typeof value !== "string") {
136
- throw literalShapeError(context, "string", value)
137
- }
138
- if (!value.isWellFormed()) {
139
- throw literalShapeError(context, "well-formed string", value)
140
- }
141
- return value
142
- }
143
- case "f64": {
144
- if (typeof value !== "number") {
145
- throw literalShapeError(context, "number", value)
146
- }
147
- return value
148
- }
149
- case "uuid": {
150
- if (!Uuid.isUuid(value)) {
151
- throw literalShapeError(context, "a UUID (canonical UUID text)", value)
152
- }
153
- // UUIDs cross as canonical hyphenated text, not raw bytes.
154
- return value
155
- }
156
- case "bytes": {
157
- if (!(value instanceof Uint8Array)) {
158
- throw literalShapeError(context, "Uint8Array", value)
159
- }
160
- refuseShared(context, value)
161
- if (value.byteLength !== field.width) {
162
- throw new AuthoringError({
163
- message: `${context}: bytes<${field.width}> takes exactly ${field.width} bytes (got ${value.byteLength})`
164
- })
165
- }
166
- // Owned copy at the ownership boundary: later caller mutation
167
- // cannot change an accepted cell.
168
- return Uint8Array.from(value)
169
- }
170
- case "interval": {
171
- if (field.element === "f64") {
172
- if (!isFloatIntervalValue(value)) {
173
- throw literalShapeError(context, "float interval ({ start, end } numbers)", value)
174
- }
175
- if (Number.isNaN(value.start) || Number.isNaN(value.end)) {
176
- throw new AuthoringError({ message: `${context}: a float interval endpoint cannot be NaN` })
177
- }
178
- const start = Object.is(value.start, -0) ? 0 : value.start
179
- const end = Object.is(value.end, -0) ? 0 : value.end
180
- if (!(start < end)) {
181
- throw new AuthoringError({
182
- message: `${context}: a float interval is half-open and nonempty (start < end strictly)`
183
- })
184
- }
185
- return { start, end }
186
- }
187
- if (!isIntervalValue(value)) {
188
- throw literalShapeError(context, "interval ({ start, end } bigints)", value)
189
- }
190
- return { start: value.start, end: value.end }
191
- }
90
+ if ("closed" in field) {
91
+ return BigInt(field.closed.handles.indexOf(fieldValue(context, field, value)))
192
92
  }
93
+ return fieldValue(context, field, value)
94
+ }
95
+
96
+ function factCellsOf(data: AnyRelation, fact: unknown): CellValue[] {
97
+ const record = recordValue(
98
+ `relation ${data.name}`,
99
+ fact,
100
+ relationFields(data).map((declared) => declared.name)
101
+ )
102
+ return relationFields(data).map((declared) =>
103
+ cellOf(`relation ${data.name} field ${declared.name}`, declared.field, record[declared.name])
104
+ )
193
105
  }
194
106
 
195
107
  /**
@@ -198,115 +110,39 @@ function cellOf(context: string, field: AnyField, value: unknown): CellValue {
198
110
  * Missing-field refusal and per-cell judgment are
199
111
  * {@link cellOf}'s, byte for byte.
200
112
  */
201
- function flatRowsOf(data: RelationData, facts: Iterable<object>): FlatRows {
113
+ function flatRowsOf(data: AnyRelation, facts: Iterable<object>): FlatRows {
202
114
  const cells: CellValue[] = []
203
115
  let rows = 0n
204
116
  for (const fact of facts) {
117
+ for (const cell of factCellsOf(data, fact)) cells.push(cell)
205
118
  rows += 1n
206
- const record = recordOf(fact)
207
- for (const declared of data.fields) {
208
- const value = record[declared.name]
209
- if (value === undefined) {
210
- throw new AuthoringError({ message: `relation ${data.name}: fact is missing field ${declared.name}` })
211
- }
212
- const cell = cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value)
213
- cells.push(cell)
214
- }
215
119
  }
216
120
  return { rows, cells }
217
121
  }
218
122
 
219
123
  function keyCellsOf(
220
- data: RelationData,
124
+ data: AnyRelation,
221
125
  projection: readonly string[],
222
- key: Readonly<Record<string, unknown>>
126
+ input: Readonly<Record<string, unknown>>
223
127
  ): CellValue[] {
224
- for (const supplied of Object.keys(key)) {
225
- if (key[supplied] !== undefined && !projection.includes(supplied)) {
226
- throw new AuthoringError({
227
- message: `relation ${data.name}: key object carries field ${supplied} outside the primary key projection (${projection.join(", ")})`
228
- })
229
- }
230
- }
128
+ const key = recordValue(`relation ${data.name} key`, input, projection)
231
129
  return projection.map(function marshalKeyCell(fieldName) {
232
- const declared = data.fields.find(function byName(candidate) {
233
- return candidate.name === fieldName
234
- })
130
+ const declared = relationFields(data).find((candidate) => candidate.name === fieldName)
235
131
  if (declared === undefined) {
236
- throw new AuthoringError({
237
- message: `relation ${data.name}: key projection cites unknown field ${fieldName}`
238
- })
132
+ throw new AuthoringError({ message: `relation ${data.name}: key projection cites unknown field ${fieldName}` })
239
133
  }
240
- const value = key[fieldName]
241
- if (value === undefined) {
242
- throw new AuthoringError({
243
- message: `relation ${data.name}: key object is missing field ${fieldName} — get reads through the primary (first-declared) key, whose projection is (${projection.join(", ")})`
244
- })
245
- }
246
- return cellOf(`relation ${data.name} key field ${fieldName}`, declared.field, value)
134
+ return cellOf(`relation ${data.name} key field ${fieldName}`, declared.field, key[fieldName])
247
135
  })
248
136
  }
249
137
 
250
138
  function decodeCell(context: string, field: AnyField, cell: unknown): unknown {
251
139
  const roster = rosterOf(field)
252
- if (roster !== undefined) {
253
- return handleOf(context, roster, cell)
254
- }
255
- switch (field.kind) {
256
- case "bool": {
257
- if (typeof cell !== "boolean") {
258
- throw new SdkInvariantError({ message: `${context}: expected boolean cell` })
259
- }
260
- return cell
261
- }
262
- case "u64":
263
- case "i64": {
264
- if (typeof cell !== "bigint") {
265
- throw new SdkInvariantError({ message: `${context}: expected bigint cell` })
266
- }
267
- return cell
268
- }
269
- case "f64": {
270
- if (typeof cell !== "number") {
271
- throw new SdkInvariantError({ message: `${context}: expected number cell` })
272
- }
273
- return cell
274
- }
275
- case "str": {
276
- if (typeof cell !== "string") {
277
- throw new SdkInvariantError({ message: `${context}: expected string cell` })
278
- }
279
- return cell
280
- }
281
- case "uuid": {
282
- // The native renderer spells uuid as the canonical
283
- // canonical hyphenated UUID string (marshal.rs `uuid_hex`); the read
284
- // side revalidates the spelling and keeps the string value.
285
- if (!Uuid.isUuid(cell)) {
286
- throw new SdkInvariantError({
287
- message: `${context}: expected an uuid cell (canonical UUID text)`
288
- })
289
- }
290
- return cell
291
- }
292
- case "bytes": {
293
- if (!(cell instanceof Uint8Array)) {
294
- throw new SdkInvariantError({ message: `${context}: expected owned bytes` })
295
- }
296
- return cell
297
- }
298
- case "interval": {
299
- if (field.element === "f64") {
300
- if (!isFloatIntervalValue(cell)) {
301
- throw new SdkInvariantError({ message: `${context}: expected a float interval cell` })
302
- }
303
- return Object.freeze({ start: cell.start, end: cell.end })
304
- }
305
- if (!isIntervalValue(cell)) {
306
- throw new SdkInvariantError({ message: `${context}: expected an integer interval cell` })
307
- }
308
- return Object.freeze({ start: cell.start, end: cell.end })
309
- }
140
+ if (roster !== undefined) return handleOf(context, roster, cell)
141
+ try {
142
+ return fieldValue(context, field, cell)
143
+ } catch (cause) {
144
+ if (cause instanceof AuthoringError) throw new SdkInvariantError({ message: cause.message })
145
+ throw cause
310
146
  }
311
147
  }
312
148
 
@@ -314,7 +150,7 @@ function isCompleteFact<R extends AnyRelation>(
314
150
  relation: R,
315
151
  decoded: Readonly<Record<string, unknown>>
316
152
  ): decoded is Readonly<Record<string, unknown>> & Fact<R> {
317
- return relation.data.fields.every(function present(declared) {
153
+ return relationFields(relation).every(function present(declared) {
318
154
  return decoded[declared.name] !== undefined
319
155
  })
320
156
  }
@@ -324,21 +160,22 @@ function isCompleteFact<R extends AnyRelation>(
324
160
  * declared field order: stable field names and shapes, no Proxy or per-cell closures.
325
161
  */
326
162
  function factOfCells<R extends AnyRelation>(relation: R, row: readonly unknown[]): Fact<R> {
327
- const data = relation.data
328
- if (row.length !== data.fields.length) {
163
+ const data = relation
164
+ if (row.length !== relationFields(data).length) {
329
165
  throw new SdkInvariantError({
330
- message: `relation ${data.name}: row arity ${row.length} does not match the ${data.fields.length} declared fields`
166
+ message: `relation ${data.name}: row arity ${row.length} does not match the ${relationFields(data).length} declared fields`
331
167
  })
332
168
  }
333
169
  const decoded: Record<string, unknown> = {}
334
- data.fields.forEach(function decodeOne(declared, ordinal) {
170
+ relationFields(data).forEach(function decodeOne(declared, ordinal) {
335
171
  const cell = row[ordinal]
336
172
  if (cell === undefined) {
337
173
  throw new SdkInvariantError({
338
174
  message: `relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`
339
175
  })
340
176
  }
341
- decoded[declared.name] = decodeCell(`relation ${data.name} field ${declared.name}`, declared.field, cell)
177
+ const value = decodeCell(`relation ${data.name} field ${declared.name}`, declared.field, cell)
178
+ setOwnField(decoded, declared.name, value)
342
179
  })
343
180
  Object.freeze(decoded)
344
181
  if (!isCompleteFact(relation, decoded)) {
@@ -347,5 +184,25 @@ function factOfCells<R extends AnyRelation>(relation: R, row: readonly unknown[]
347
184
  return decoded
348
185
  }
349
186
 
187
+ /** Preserve every declared name as data, including JavaScript's prototype setter. */
188
+ function setOwnField(record: Record<string, unknown>, name: string, value: unknown): void {
189
+ if (name === "__proto__") {
190
+ Object.defineProperty(record, name, { value, enumerable: true, writable: true, configurable: true })
191
+ } else {
192
+ record[name] = value
193
+ }
194
+ }
195
+
350
196
  export type { CellValue, FlatRows }
351
- export { cellOf, decodeCell, factOfCells, flatRowsOf, handleOf, hostCellCharge, keyCellsOf, recordOf }
197
+ export {
198
+ cellOf,
199
+ decodeCell,
200
+ factCellsOf,
201
+ factOfCells,
202
+ flatRowsOf,
203
+ handleOf,
204
+ hostCellCharge,
205
+ keyCellsOf,
206
+ recordOf,
207
+ setOwnField
208
+ }
@@ -1,4 +1,5 @@
1
1
  import { Schema } from "effect"
2
+ import { AuthoringDiagnostic, AuthoringError } from "#errors.ts"
2
3
  import { runtimeErrorCodes } from "#runtime-codes.ts"
3
4
 
4
5
  export { runtimeErrorCodes } from "#runtime-codes.ts"
@@ -12,7 +13,18 @@ const ResourceLimit = Schema.Struct({
12
13
  })
13
14
  const PlainReason = Schema.Struct({
14
15
  _tag: Schema.Literals(
15
- runtimeErrorCodes.filter((code) => code !== "ResourceLimit" && code !== "Io" && code !== "Engine")
16
+ runtimeErrorCodes.filter(
17
+ (code) => code !== "ResourceLimit" && code !== "Io" && code !== "Engine" && code !== "InvalidArgument"
18
+ )
19
+ )
20
+ })
21
+ const InvalidArgument = Schema.Struct({
22
+ _tag: Schema.Literal("InvalidArgument"),
23
+ detail: Schema.optional(
24
+ Schema.Struct({
25
+ message: Schema.String,
26
+ diagnostic: Schema.optional(AuthoringDiagnostic)
27
+ })
16
28
  )
17
29
  })
18
30
  const Io = Schema.Struct({
@@ -26,7 +38,7 @@ const Engine = Schema.Struct({
26
38
  kind: Schema.String,
27
39
  message: Schema.String
28
40
  })
29
- export const DbReason = Schema.Union([ResourceLimit, Io, Engine, PlainReason])
41
+ export const DbReason = Schema.Union([ResourceLimit, Io, Engine, InvalidArgument, PlainReason])
30
42
  export class DbError extends Schema.TaggedError<DbError>()("DbError", {
31
43
  operation: Schema.String,
32
44
  reason: DbReason
@@ -34,12 +46,44 @@ export class DbError extends Schema.TaggedError<DbError>()("DbError", {
34
46
  get code() {
35
47
  return this.reason._tag
36
48
  }
49
+ /** Safe default display; callers explicitly inspect structured detail. */
50
+ override get message(): string {
51
+ return `${this.operation}: ${this.code}`
52
+ }
37
53
  }
38
54
 
39
55
  const decodeReason = Schema.decodeUnknownOption(DbReason)
40
56
  export function dbError(operation: string, cause: unknown): DbError {
57
+ if (cause instanceof DbError) return cause
58
+ if (cause instanceof AuthoringError) return argumentError(operation, cause)
41
59
  const decoded = decodeReason(cause)
42
- return new DbError({ operation, reason: decoded._tag === "Some" ? decoded.value : { _tag: "Internal" } })
60
+ if (decoded._tag === "Some") return new DbError({ operation, reason: decoded.value })
61
+ // Synchronous native schema/marshalling refusals precede the executor's
62
+ // tagged error wire. Preserve their engine family instead of losing it.
63
+ if (
64
+ typeof cause === "object" &&
65
+ cause !== null &&
66
+ "kind" in cause &&
67
+ typeof cause.kind === "string" &&
68
+ "message" in cause &&
69
+ typeof cause.message === "string"
70
+ ) {
71
+ return new DbError({ operation, reason: { _tag: "Engine", kind: cause.kind, message: cause.message } })
72
+ }
73
+ return new DbError({ operation, reason: { _tag: "Internal" } })
74
+ }
75
+
76
+ /** Host input failures retain authoring details without displaying values. */
77
+ export function argumentError(operation: string, cause: unknown): DbError {
78
+ if (cause instanceof DbError) return cause
79
+ if (cause instanceof AuthoringError) {
80
+ const detail =
81
+ cause.diagnostic === undefined
82
+ ? { message: cause.message }
83
+ : { message: cause.message, diagnostic: cause.diagnostic }
84
+ return new DbError({ operation, reason: { _tag: "InvalidArgument", detail } })
85
+ }
86
+ return new DbError({ operation, reason: { _tag: "InvalidArgument" } })
43
87
  }
44
88
 
45
89
  const Outstanding = Schema.Struct({
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 }