@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.
- package/COOKBOOK.md +114 -11
- package/README.md +52 -9
- package/dist/capacity.d.ts +15 -55
- package/dist/capacity.d.ts.map +1 -1
- package/dist/capacity.js +63 -12
- 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 +14 -10
- 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 +10 -10
- 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/lower.d.ts +60 -4
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +126 -161
- 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 +3 -4
- 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 +6 -9
- 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 +86 -76
- 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 +34 -28
- package/src/internal/log.ts +2 -0
- package/src/law.ts +13 -13
- 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/lower.ts +139 -173
- package/src/query/parse-ir.ts +247 -54
- package/src/query/run.ts +15 -9
- package/src/query/scope.ts +10 -18
- 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 +9 -9
- package/src/statements.ts +138 -189
- 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 {
|
|
21
|
-
import type
|
|
22
|
-
import {
|
|
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"
|
|
48
|
-
throw new AuthoringError({ message: "
|
|
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
|
-
|
|
114
|
-
|
|
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:
|
|
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:
|
|
124
|
+
data: AnyRelation,
|
|
221
125
|
projection: readonly string[],
|
|
222
|
-
|
|
126
|
+
input: Readonly<Record<string, unknown>>
|
|
223
127
|
): CellValue[] {
|
|
224
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
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
|
|
328
|
-
if (row.length !== data.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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 {
|
|
197
|
+
export {
|
|
198
|
+
cellOf,
|
|
199
|
+
decodeCell,
|
|
200
|
+
factCellsOf,
|
|
201
|
+
factOfCells,
|
|
202
|
+
flatRowsOf,
|
|
203
|
+
handleOf,
|
|
204
|
+
hostCellCharge,
|
|
205
|
+
keyCellsOf,
|
|
206
|
+
recordOf,
|
|
207
|
+
setOwnField
|
|
208
|
+
}
|
package/src/runtime-errors.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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 {
|
|
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 }
|