@bjornpagen/bumbledb-log 0.17.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/README.md +119 -0
- package/package.json +58 -0
- package/src/braids.ts +38 -0
- package/src/bytes.ts +236 -0
- package/src/chain.ts +109 -0
- package/src/codec.ts +256 -0
- package/src/descriptor.ts +741 -0
- package/src/errors.ts +150 -0
- package/src/index.ts +38 -0
- package/src/keys.ts +34 -0
- package/src/manifest.ts +113 -0
- package/src/replica.ts +796 -0
- package/src/store.ts +292 -0
- package/src/tenants.ts +140 -0
- package/src/value.ts +285 -0
- package/src/writer.ts +506 -0
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parsed protocol descriptor: the SDK's lowered `SchemaSpec` resolved
|
|
3
|
+
* once — names to dense ids, handles to row ids, statements materialized
|
|
4
|
+
* in the engine's own order (fresh auto-keys first, closed auto-keys,
|
|
5
|
+
* then declared statements with `==` split into two containments) — so
|
|
6
|
+
* every pure function downstream reads ids, never names. Braid
|
|
7
|
+
* derivation and the schema fingerprint mirror live on the same parse:
|
|
8
|
+
* one boundary, parsed in full, cached per theory value.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AnySchema,
|
|
13
|
+
LiteralSetSpec,
|
|
14
|
+
LiteralSpec,
|
|
15
|
+
SchemaSpec,
|
|
16
|
+
SideSpec,
|
|
17
|
+
StatementSpec,
|
|
18
|
+
ValueSpec,
|
|
19
|
+
ValueTypeSpec
|
|
20
|
+
} from "@bjornpagen/bumbledb"
|
|
21
|
+
import { internalBlake3, lower } from "@bjornpagen/bumbledb"
|
|
22
|
+
import * as errors from "@superbuilders/errors"
|
|
23
|
+
import { ByteWriter, fromHex, toHex, utf8Encoder } from "#bytes.ts"
|
|
24
|
+
import type { LogValue } from "#value.ts"
|
|
25
|
+
import { writeCanonicalLiteral } from "#value.ts"
|
|
26
|
+
|
|
27
|
+
interface FieldInfo {
|
|
28
|
+
readonly name: string
|
|
29
|
+
readonly type: ValueTypeSpec
|
|
30
|
+
readonly fresh: boolean
|
|
31
|
+
/** Set when the field's newtype names a closed relation's id class. */
|
|
32
|
+
readonly closedRef: string | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RelationInfo {
|
|
36
|
+
readonly id: number
|
|
37
|
+
readonly name: string
|
|
38
|
+
readonly closed: boolean
|
|
39
|
+
readonly handles: readonly string[]
|
|
40
|
+
/** Sealed order: a closed relation's synthetic u64 `id` at ordinal 0. */
|
|
41
|
+
readonly fields: readonly FieldInfo[]
|
|
42
|
+
/** Closed ground axioms in sealed order (id first), resolved values. */
|
|
43
|
+
readonly rows: ReadonlyArray<readonly LogValue[]>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SideInfo {
|
|
47
|
+
readonly relation: number
|
|
48
|
+
readonly projection: readonly number[]
|
|
49
|
+
readonly selection: ReadonlyArray<{ readonly field: number; readonly values: readonly LogValue[] }>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type WeightInfo =
|
|
53
|
+
| { readonly kind: "unit" }
|
|
54
|
+
| { readonly kind: "field"; readonly field: number }
|
|
55
|
+
| { readonly kind: "duration"; readonly field: number }
|
|
56
|
+
|
|
57
|
+
type HiInfo =
|
|
58
|
+
| { readonly kind: "unbounded" }
|
|
59
|
+
| { readonly kind: "lit"; readonly value: bigint }
|
|
60
|
+
| { readonly kind: "targetField"; readonly field: number }
|
|
61
|
+
| { readonly kind: "targetDuration"; readonly field: number }
|
|
62
|
+
|
|
63
|
+
type StatementInfo =
|
|
64
|
+
| {
|
|
65
|
+
readonly id: number
|
|
66
|
+
readonly kind: "functionality"
|
|
67
|
+
readonly relation: number
|
|
68
|
+
readonly projection: readonly number[]
|
|
69
|
+
}
|
|
70
|
+
| { readonly id: number; readonly kind: "containment"; readonly source: SideInfo; readonly target: SideInfo }
|
|
71
|
+
| {
|
|
72
|
+
readonly id: number
|
|
73
|
+
readonly kind: "capacity"
|
|
74
|
+
readonly target: SideInfo
|
|
75
|
+
readonly weight: WeightInfo
|
|
76
|
+
readonly lo: bigint
|
|
77
|
+
readonly hi: HiInfo
|
|
78
|
+
readonly source: SideInfo
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface SerialStatement {
|
|
82
|
+
readonly statement: number
|
|
83
|
+
readonly braid: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface LogDescriptor {
|
|
87
|
+
readonly relations: readonly RelationInfo[]
|
|
88
|
+
readonly relationByName: ReadonlyMap<string, RelationInfo>
|
|
89
|
+
readonly statements: readonly StatementInfo[]
|
|
90
|
+
/** Ordinary relation id → braid id string (`c{smallest:08x}`). */
|
|
91
|
+
readonly braidOfRelation: ReadonlyMap<number, string>
|
|
92
|
+
/** Braid id string → member relation ids, ascending. */
|
|
93
|
+
readonly braidMembers: ReadonlyMap<string, readonly number[]>
|
|
94
|
+
readonly serialAtStatements: readonly SerialStatement[]
|
|
95
|
+
readonly fingerprint: string
|
|
96
|
+
readonly fingerprintBytes: Uint8Array
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The pure trio's input: the theory value, its lowered spec, or an already-parsed descriptor. */
|
|
100
|
+
type LogTheory = AnySchema | SchemaSpec | LogDescriptor
|
|
101
|
+
|
|
102
|
+
function isDescriptor(theory: LogTheory): theory is LogDescriptor {
|
|
103
|
+
return "braidMembers" in theory
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isSpec(theory: LogTheory): theory is SchemaSpec {
|
|
107
|
+
return Array.isArray((theory as SchemaSpec).relations)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cache = new WeakMap<object, LogDescriptor>()
|
|
111
|
+
|
|
112
|
+
function descriptorOf(theory: LogTheory): LogDescriptor {
|
|
113
|
+
if (isDescriptor(theory)) {
|
|
114
|
+
return theory
|
|
115
|
+
}
|
|
116
|
+
const hit = cache.get(theory)
|
|
117
|
+
if (hit !== undefined) {
|
|
118
|
+
return hit
|
|
119
|
+
}
|
|
120
|
+
const spec = isSpec(theory) ? theory : lower(theory)
|
|
121
|
+
const parsed = spec === theory ? parseSpec(spec) : (cache.get(spec) ?? parseSpec(spec))
|
|
122
|
+
cache.set(theory, parsed)
|
|
123
|
+
cache.set(spec, parsed)
|
|
124
|
+
return parsed
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function braidHex(relationId: number): string {
|
|
128
|
+
return `c${relationId.toString(16).padStart(8, "0")}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function logValueOf(value: ValueSpec): LogValue {
|
|
132
|
+
switch (value.kind) {
|
|
133
|
+
case "bool":
|
|
134
|
+
return value.value
|
|
135
|
+
case "u64":
|
|
136
|
+
case "i64":
|
|
137
|
+
return value.value
|
|
138
|
+
case "string":
|
|
139
|
+
return value.value
|
|
140
|
+
case "fixedBytes":
|
|
141
|
+
return value.value
|
|
142
|
+
case "intervalU64":
|
|
143
|
+
case "intervalI64":
|
|
144
|
+
return { start: value.start, end: value.end }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface SpecTables {
|
|
149
|
+
readonly spec: SchemaSpec
|
|
150
|
+
readonly relations: RelationInfo[]
|
|
151
|
+
readonly byName: Map<string, RelationInfo>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fieldsOf(
|
|
155
|
+
tables: Map<string, { closed: boolean; handles: readonly string[] }>,
|
|
156
|
+
relation: SchemaSpec["relations"][number]
|
|
157
|
+
): FieldInfo[] {
|
|
158
|
+
const fields: FieldInfo[] = []
|
|
159
|
+
if (relation.closed !== undefined) {
|
|
160
|
+
fields.push({
|
|
161
|
+
name: "id",
|
|
162
|
+
type: { kind: "u64" },
|
|
163
|
+
fresh: false,
|
|
164
|
+
closedRef: relation.name
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
for (const field of relation.fields) {
|
|
168
|
+
let closedRef: string | undefined
|
|
169
|
+
if (field.newtype?.endsWith(".id")) {
|
|
170
|
+
const owner = field.newtype.slice(0, -3)
|
|
171
|
+
const target = tables.get(owner)
|
|
172
|
+
if (target?.closed === true) {
|
|
173
|
+
closedRef = owner
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
fields.push({ name: field.name, type: field.valueType, fresh: field.fresh, closedRef })
|
|
177
|
+
}
|
|
178
|
+
return fields
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function resolveLiteral(tables: SpecTables, relation: RelationInfo, field: FieldInfo, literal: LiteralSpec): LogValue {
|
|
182
|
+
if (literal.kind === "value") {
|
|
183
|
+
return logValueOf(literal.value)
|
|
184
|
+
}
|
|
185
|
+
if (field.closedRef === undefined) {
|
|
186
|
+
throw errors.new(
|
|
187
|
+
`handle literal ${literal.handle} on ${relation.name}.${field.name}, which references no closed roster`
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
const target = tables.byName.get(field.closedRef)
|
|
191
|
+
if (target === undefined) {
|
|
192
|
+
throw errors.new(`handle literal ${literal.handle}: unknown closed relation ${field.closedRef}`)
|
|
193
|
+
}
|
|
194
|
+
const id = target.handles.indexOf(literal.handle)
|
|
195
|
+
if (id === -1) {
|
|
196
|
+
throw errors.new(`handle literal ${literal.handle} is not in the ${target.name} roster`)
|
|
197
|
+
}
|
|
198
|
+
return BigInt(id)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function literalSetOf(tables: SpecTables, relation: RelationInfo, field: FieldInfo, set: LiteralSetSpec): LogValue[] {
|
|
202
|
+
if (set.kind === "one") {
|
|
203
|
+
return [resolveLiteral(tables, relation, field, set.literal)]
|
|
204
|
+
}
|
|
205
|
+
return set.literals.map(function resolveEach(literal) {
|
|
206
|
+
return resolveLiteral(tables, relation, field, literal)
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fieldOrdinal(relation: RelationInfo, name: string): number {
|
|
211
|
+
const ordinal = relation.fields.findIndex(function byName(field) {
|
|
212
|
+
return field.name === name
|
|
213
|
+
})
|
|
214
|
+
if (ordinal === -1) {
|
|
215
|
+
throw errors.new(`relation ${relation.name} has no field ${name}`)
|
|
216
|
+
}
|
|
217
|
+
return ordinal
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sideOf(tables: SpecTables, side: SideSpec): SideInfo {
|
|
221
|
+
const relation = tables.byName.get(side.relation)
|
|
222
|
+
if (relation === undefined) {
|
|
223
|
+
throw errors.new(`statement cites unknown relation ${side.relation}`)
|
|
224
|
+
}
|
|
225
|
+
const projection = side.projection.map(function ordinalOf(name) {
|
|
226
|
+
return fieldOrdinal(relation, name)
|
|
227
|
+
})
|
|
228
|
+
const selection = side.selection.map(function bindingOf(binding) {
|
|
229
|
+
const ordinal = fieldOrdinal(relation, binding[0])
|
|
230
|
+
const field = relation.fields[ordinal]
|
|
231
|
+
if (field === undefined) {
|
|
232
|
+
throw errors.new(`relation ${relation.name} has no field ordinal ${ordinal}`)
|
|
233
|
+
}
|
|
234
|
+
return { field: ordinal, values: literalSetOf(tables, relation, field, binding[1]) }
|
|
235
|
+
})
|
|
236
|
+
return { relation: relation.id, projection, selection }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function boundValue(context: string, bound: { readonly kind: string }): bigint {
|
|
240
|
+
if (bound.kind !== "lit") {
|
|
241
|
+
throw errors.new(`${context}: dependent floors are refused by the schema grammar`)
|
|
242
|
+
}
|
|
243
|
+
return (bound as { readonly kind: "lit"; readonly value: bigint }).value
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function capacityOf(
|
|
247
|
+
tables: SpecTables,
|
|
248
|
+
id: number,
|
|
249
|
+
statement: Extract<StatementSpec, { kind: "capacity" }>
|
|
250
|
+
): StatementInfo {
|
|
251
|
+
const target = sideOf(tables, statement.target)
|
|
252
|
+
const source = sideOf(tables, statement.source)
|
|
253
|
+
const sourceRelation = tables.byName.get(statement.source.relation)
|
|
254
|
+
const targetRelation = tables.byName.get(statement.target.relation)
|
|
255
|
+
if (sourceRelation === undefined || targetRelation === undefined) {
|
|
256
|
+
throw errors.new("capacity statement cites unknown relations")
|
|
257
|
+
}
|
|
258
|
+
let weight: WeightInfo
|
|
259
|
+
switch (statement.weight.kind) {
|
|
260
|
+
case "unit": {
|
|
261
|
+
weight = { kind: "unit" }
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
case "field": {
|
|
265
|
+
weight = { kind: "field", field: fieldOrdinal(sourceRelation, statement.weight.field) }
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
case "durationField": {
|
|
269
|
+
weight = { kind: "duration", field: fieldOrdinal(sourceRelation, statement.weight.field) }
|
|
270
|
+
break
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let lo: bigint
|
|
274
|
+
let hi: HiInfo
|
|
275
|
+
const window = statement.window
|
|
276
|
+
switch (window.kind) {
|
|
277
|
+
case "exact": {
|
|
278
|
+
lo = boundValue("capacity exact bound", window.n)
|
|
279
|
+
hi = { kind: "lit", value: lo }
|
|
280
|
+
break
|
|
281
|
+
}
|
|
282
|
+
case "floor": {
|
|
283
|
+
lo = boundValue("capacity floor", window.lo)
|
|
284
|
+
hi = { kind: "unbounded" }
|
|
285
|
+
break
|
|
286
|
+
}
|
|
287
|
+
case "range": {
|
|
288
|
+
lo = boundValue("capacity floor", window.lo)
|
|
289
|
+
switch (window.hi.kind) {
|
|
290
|
+
case "lit": {
|
|
291
|
+
hi = { kind: "lit", value: window.hi.value }
|
|
292
|
+
break
|
|
293
|
+
}
|
|
294
|
+
case "field": {
|
|
295
|
+
hi = { kind: "targetField", field: fieldOrdinal(targetRelation, window.hi.field) }
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
case "durationField": {
|
|
299
|
+
hi = { kind: "targetDuration", field: fieldOrdinal(targetRelation, window.hi.field) }
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return { id, kind: "capacity", target, weight, lo, hi, source }
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The `exact {n}` window: lo = hi = n; a non-literal n is grammar-refused upstream. */
|
|
310
|
+
|
|
311
|
+
function parseSpec(spec: SchemaSpec): LogDescriptor {
|
|
312
|
+
const prepass = new Map<string, { closed: boolean; handles: readonly string[] }>()
|
|
313
|
+
for (const relation of spec.relations) {
|
|
314
|
+
prepass.set(relation.name, {
|
|
315
|
+
closed: relation.closed !== undefined,
|
|
316
|
+
handles: relation.closed === undefined ? [] : relation.closed.rows.map((row) => row.handle)
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const relations: RelationInfo[] = []
|
|
321
|
+
const byName = new Map<string, RelationInfo>()
|
|
322
|
+
spec.relations.forEach(function buildRelation(relation, id) {
|
|
323
|
+
const fields = fieldsOf(prepass, relation)
|
|
324
|
+
const info: RelationInfo = {
|
|
325
|
+
id,
|
|
326
|
+
name: relation.name,
|
|
327
|
+
closed: relation.closed !== undefined,
|
|
328
|
+
handles: prepass.get(relation.name)?.handles ?? [],
|
|
329
|
+
fields,
|
|
330
|
+
rows: []
|
|
331
|
+
}
|
|
332
|
+
relations.push(info)
|
|
333
|
+
byName.set(relation.name, info)
|
|
334
|
+
})
|
|
335
|
+
const tables: SpecTables = { spec, relations, byName }
|
|
336
|
+
|
|
337
|
+
spec.relations.forEach(function resolveRows(relation, id) {
|
|
338
|
+
if (relation.closed === undefined) {
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
const info = relations[id]
|
|
342
|
+
if (info === undefined) {
|
|
343
|
+
throw errors.new(`relation ordinal ${id} missing`)
|
|
344
|
+
}
|
|
345
|
+
const rows = relation.closed.rows.map(function resolveRow(row, rowId) {
|
|
346
|
+
const values: LogValue[] = [BigInt(rowId)]
|
|
347
|
+
row.values.forEach(function resolveCell(literal, column) {
|
|
348
|
+
const field = info.fields[column + 1]
|
|
349
|
+
if (field === undefined) {
|
|
350
|
+
throw errors.new(`closed row ${row.handle} of ${relation.name}: no field at column ${column}`)
|
|
351
|
+
}
|
|
352
|
+
values.push(resolveLiteral(tables, info, field, literal))
|
|
353
|
+
})
|
|
354
|
+
return values
|
|
355
|
+
})
|
|
356
|
+
relations[id] = { ...info, rows }
|
|
357
|
+
byName.set(relation.name, relations[id])
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const statements: StatementInfo[] = []
|
|
361
|
+
relations.forEach(function freshKeys(relation) {
|
|
362
|
+
relation.fields.forEach(function freshKey(field, ordinal) {
|
|
363
|
+
if (field.fresh) {
|
|
364
|
+
statements.push({ id: statements.length, kind: "functionality", relation: relation.id, projection: [ordinal] })
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
relations.forEach(function closedKeys(relation) {
|
|
369
|
+
if (relation.closed) {
|
|
370
|
+
statements.push({ id: statements.length, kind: "functionality", relation: relation.id, projection: [0] })
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
for (const statement of spec.statements) {
|
|
374
|
+
switch (statement.kind) {
|
|
375
|
+
case "fd": {
|
|
376
|
+
const relation = byName.get(statement.relation)
|
|
377
|
+
if (relation === undefined) {
|
|
378
|
+
throw errors.new(`key statement cites unknown relation ${statement.relation}`)
|
|
379
|
+
}
|
|
380
|
+
statements.push({
|
|
381
|
+
id: statements.length,
|
|
382
|
+
kind: "functionality",
|
|
383
|
+
relation: relation.id,
|
|
384
|
+
projection: statement.projection.map(function ordinalOf(name) {
|
|
385
|
+
return fieldOrdinal(relation, name)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
case "containment": {
|
|
391
|
+
const source = sideOf(tables, statement.source)
|
|
392
|
+
const target = sideOf(tables, statement.target)
|
|
393
|
+
statements.push({ id: statements.length, kind: "containment", source, target })
|
|
394
|
+
if (statement.bidirectional) {
|
|
395
|
+
statements.push({ id: statements.length, kind: "containment", source: target, target: source })
|
|
396
|
+
}
|
|
397
|
+
break
|
|
398
|
+
}
|
|
399
|
+
case "capacity": {
|
|
400
|
+
statements.push(capacityOf(tables, statements.length, statement))
|
|
401
|
+
break
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const { braidOfRelation, braidMembers } = deriveBraids(relations, statements)
|
|
407
|
+
|
|
408
|
+
const serialAtStatements: SerialStatement[] = []
|
|
409
|
+
for (const statement of statements) {
|
|
410
|
+
if (statement.kind === "functionality" && statement.projection.length === 0) {
|
|
411
|
+
const braid = braidOfRelation.get(statement.relation)
|
|
412
|
+
if (braid !== undefined) {
|
|
413
|
+
serialAtStatements.push({ statement: statement.id, braid })
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (statement.kind === "capacity" && statement.target.projection.length === 0) {
|
|
417
|
+
const targetRelation = relations[statement.target.relation]
|
|
418
|
+
if (targetRelation !== undefined && !targetRelation.closed) {
|
|
419
|
+
const braid = braidOfRelation.get(statement.target.relation)
|
|
420
|
+
if (braid !== undefined) {
|
|
421
|
+
serialAtStatements.push({ statement: statement.id, braid })
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
let hashed: { readonly hex: string; readonly bytes: Uint8Array } | undefined
|
|
428
|
+
function fingerprintLazily(): { readonly hex: string; readonly bytes: Uint8Array } {
|
|
429
|
+
if (hashed === undefined) {
|
|
430
|
+
const bytes = fingerprintOf(relations, statements)
|
|
431
|
+
hashed = { hex: toHex(bytes), bytes }
|
|
432
|
+
}
|
|
433
|
+
return hashed
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const descriptor: LogDescriptor = {
|
|
437
|
+
relations,
|
|
438
|
+
relationByName: byName,
|
|
439
|
+
statements,
|
|
440
|
+
braidOfRelation,
|
|
441
|
+
braidMembers,
|
|
442
|
+
serialAtStatements,
|
|
443
|
+
get fingerprint() {
|
|
444
|
+
return fingerprintLazily().hex
|
|
445
|
+
},
|
|
446
|
+
get fingerprintBytes() {
|
|
447
|
+
return fingerprintLazily().bytes
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return descriptor
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* The same descriptor under a pinned fingerprint — for stores whose
|
|
455
|
+
* identity is carried (a manifest, a conformance sidecar) rather than
|
|
456
|
+
* recomputed, e.g. when the mirror cannot hash a closed relation's
|
|
457
|
+
* interned string axioms.
|
|
458
|
+
*/
|
|
459
|
+
function withFingerprint(descriptor: LogDescriptor, fingerprint: string): LogDescriptor {
|
|
460
|
+
return {
|
|
461
|
+
relations: descriptor.relations,
|
|
462
|
+
relationByName: descriptor.relationByName,
|
|
463
|
+
statements: descriptor.statements,
|
|
464
|
+
braidOfRelation: descriptor.braidOfRelation,
|
|
465
|
+
braidMembers: descriptor.braidMembers,
|
|
466
|
+
serialAtStatements: descriptor.serialAtStatements,
|
|
467
|
+
fingerprint,
|
|
468
|
+
fingerprintBytes: fromHex(fingerprint)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function deriveBraids(
|
|
473
|
+
relations: readonly RelationInfo[],
|
|
474
|
+
statements: readonly StatementInfo[]
|
|
475
|
+
): { braidOfRelation: Map<number, string>; braidMembers: Map<string, readonly number[]> } {
|
|
476
|
+
const parent = new Map<number, number>()
|
|
477
|
+
for (const relation of relations) {
|
|
478
|
+
if (!relation.closed) {
|
|
479
|
+
parent.set(relation.id, relation.id)
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
function rootOf(id: number): number {
|
|
483
|
+
let cursor = id
|
|
484
|
+
for (;;) {
|
|
485
|
+
const up = parent.get(cursor)
|
|
486
|
+
if (up === undefined || up === cursor) {
|
|
487
|
+
return cursor
|
|
488
|
+
}
|
|
489
|
+
cursor = up
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
function union(a: number, b: number): void {
|
|
493
|
+
const ra = rootOf(a)
|
|
494
|
+
const rb = rootOf(b)
|
|
495
|
+
if (ra !== rb) {
|
|
496
|
+
parent.set(Math.max(ra, rb), Math.min(ra, rb))
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
for (const statement of statements) {
|
|
500
|
+
if (statement.kind === "functionality") {
|
|
501
|
+
continue
|
|
502
|
+
}
|
|
503
|
+
const source = relations[statement.source.relation]
|
|
504
|
+
const target = relations[statement.target.relation]
|
|
505
|
+
if (source === undefined || target === undefined || source.closed || target.closed) {
|
|
506
|
+
continue
|
|
507
|
+
}
|
|
508
|
+
union(source.id, target.id)
|
|
509
|
+
}
|
|
510
|
+
const members = new Map<number, number[]>()
|
|
511
|
+
for (const id of parent.keys()) {
|
|
512
|
+
const root = rootOf(id)
|
|
513
|
+
const list = members.get(root)
|
|
514
|
+
if (list === undefined) {
|
|
515
|
+
members.set(root, [id])
|
|
516
|
+
} else {
|
|
517
|
+
list.push(id)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const braidOfRelation = new Map<number, string>()
|
|
521
|
+
const braidMembers = new Map<string, readonly number[]>()
|
|
522
|
+
for (const [root, ids] of [...members.entries()].sort(function byRoot(a, b) {
|
|
523
|
+
return a[0] - b[0]
|
|
524
|
+
})) {
|
|
525
|
+
const braid = braidHex(root)
|
|
526
|
+
ids.sort(function ascending(a, b) {
|
|
527
|
+
return a - b
|
|
528
|
+
})
|
|
529
|
+
braidMembers.set(braid, ids)
|
|
530
|
+
for (const id of ids) {
|
|
531
|
+
braidOfRelation.set(id, braid)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return { braidOfRelation, braidMembers }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** `bumbledb-schema-v5` canonical bytes, mirrored from the engine's own encoder. */
|
|
538
|
+
const FORMAT_VERSION_LABEL = "bumbledb-schema-v5"
|
|
539
|
+
|
|
540
|
+
const VALUE_TYPE_TAG = { bool: 0, u64: 2, i64: 3, string: 4, fixedBytes: 5, interval: 6, fixedInterval: 7 } as const
|
|
541
|
+
|
|
542
|
+
function putLen(out: ByteWriter, len: number): void {
|
|
543
|
+
out.u32le(len)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function putBytes(out: ByteWriter, raw: Uint8Array): void {
|
|
547
|
+
putLen(out, raw.length)
|
|
548
|
+
out.bytes(raw)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function putString(out: ByteWriter, text: string): void {
|
|
552
|
+
putBytes(out, utf8Encoder.encode(text))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function putValueType(out: ByteWriter, type: ValueTypeSpec): void {
|
|
556
|
+
switch (type.kind) {
|
|
557
|
+
case "bool": {
|
|
558
|
+
out.u8(VALUE_TYPE_TAG.bool)
|
|
559
|
+
return
|
|
560
|
+
}
|
|
561
|
+
case "u64": {
|
|
562
|
+
out.u8(VALUE_TYPE_TAG.u64)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
case "i64": {
|
|
566
|
+
out.u8(VALUE_TYPE_TAG.i64)
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
case "string": {
|
|
570
|
+
out.u8(VALUE_TYPE_TAG.string)
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
case "fixedBytes": {
|
|
574
|
+
out.u8(VALUE_TYPE_TAG.fixedBytes)
|
|
575
|
+
out.u16le(type.len)
|
|
576
|
+
return
|
|
577
|
+
}
|
|
578
|
+
case "interval": {
|
|
579
|
+
if (type.width === undefined) {
|
|
580
|
+
out.u8(VALUE_TYPE_TAG.interval)
|
|
581
|
+
out.u8(type.element === "u64" ? 0 : 1)
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
out.u8(VALUE_TYPE_TAG.fixedInterval)
|
|
585
|
+
out.u8(type.element === "u64" ? 0 : 1)
|
|
586
|
+
out.u64le(type.width)
|
|
587
|
+
return
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function putSide(out: ByteWriter, relations: readonly RelationInfo[], side: SideInfo): void {
|
|
593
|
+
out.u32le(side.relation)
|
|
594
|
+
putLen(out, side.projection.length)
|
|
595
|
+
for (const field of side.projection) {
|
|
596
|
+
out.u16le(field)
|
|
597
|
+
}
|
|
598
|
+
putLen(out, side.selection.length)
|
|
599
|
+
const relation = relations[side.relation]
|
|
600
|
+
if (relation === undefined) {
|
|
601
|
+
throw errors.new(`side cites unknown relation id ${side.relation}`)
|
|
602
|
+
}
|
|
603
|
+
for (const binding of side.selection) {
|
|
604
|
+
out.u16le(binding.field)
|
|
605
|
+
const field = relation.fields[binding.field]
|
|
606
|
+
if (field === undefined) {
|
|
607
|
+
throw errors.new(`selection cites unknown field ordinal ${binding.field}`)
|
|
608
|
+
}
|
|
609
|
+
putLen(out, binding.values.length)
|
|
610
|
+
for (const value of binding.values) {
|
|
611
|
+
if (typeof value === "string") {
|
|
612
|
+
putString(out, value)
|
|
613
|
+
} else {
|
|
614
|
+
writeCanonicalLiteral(out, field.type, value)
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function fingerprintOf(relations: readonly RelationInfo[], statements: readonly StatementInfo[]): Uint8Array {
|
|
621
|
+
const out = new ByteWriter(1024)
|
|
622
|
+
putString(out, FORMAT_VERSION_LABEL)
|
|
623
|
+
putLen(out, relations.length)
|
|
624
|
+
for (const relation of relations) {
|
|
625
|
+
putString(out, relation.name)
|
|
626
|
+
putLen(out, relation.fields.length)
|
|
627
|
+
for (const field of relation.fields) {
|
|
628
|
+
putString(out, field.name)
|
|
629
|
+
putValueType(out, field.type)
|
|
630
|
+
out.u8(field.fresh ? 1 : 0)
|
|
631
|
+
}
|
|
632
|
+
if (!relation.closed) {
|
|
633
|
+
out.u8(0)
|
|
634
|
+
} else {
|
|
635
|
+
out.u8(1)
|
|
636
|
+
putLen(out, relation.rows.length)
|
|
637
|
+
relation.rows.forEach(function putRow(row, rowId) {
|
|
638
|
+
const handle = relation.handles[rowId]
|
|
639
|
+
if (handle === undefined) {
|
|
640
|
+
throw errors.new(`closed relation ${relation.name}: no handle for row ${rowId}`)
|
|
641
|
+
}
|
|
642
|
+
putString(out, handle)
|
|
643
|
+
const fact = new ByteWriter(64)
|
|
644
|
+
row.forEach(function putCell(value, ordinal) {
|
|
645
|
+
const field = relation.fields[ordinal]
|
|
646
|
+
if (field === undefined) {
|
|
647
|
+
throw errors.new(`closed relation ${relation.name}: no field at ordinal ${ordinal}`)
|
|
648
|
+
}
|
|
649
|
+
if (field.type.kind === "string") {
|
|
650
|
+
throw errors.new(
|
|
651
|
+
`closed relation ${relation.name} has a string ground axiom — the fingerprint mirror does not carry interned axiom columns`
|
|
652
|
+
)
|
|
653
|
+
}
|
|
654
|
+
writeCanonicalLiteral(fact, field.type, value)
|
|
655
|
+
})
|
|
656
|
+
putBytes(out, fact.finish())
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
putLen(out, statements.length)
|
|
661
|
+
for (const statement of statements) {
|
|
662
|
+
switch (statement.kind) {
|
|
663
|
+
case "functionality": {
|
|
664
|
+
out.u8(0)
|
|
665
|
+
out.u32le(statement.relation)
|
|
666
|
+
putLen(out, statement.projection.length)
|
|
667
|
+
for (const field of statement.projection) {
|
|
668
|
+
out.u16le(field)
|
|
669
|
+
}
|
|
670
|
+
break
|
|
671
|
+
}
|
|
672
|
+
case "containment": {
|
|
673
|
+
out.u8(1)
|
|
674
|
+
putSide(out, relations, statement.source)
|
|
675
|
+
putSide(out, relations, statement.target)
|
|
676
|
+
break
|
|
677
|
+
}
|
|
678
|
+
case "capacity": {
|
|
679
|
+
out.u8(4)
|
|
680
|
+
putSide(out, relations, statement.target)
|
|
681
|
+
switch (statement.weight.kind) {
|
|
682
|
+
case "unit": {
|
|
683
|
+
out.u8(0)
|
|
684
|
+
break
|
|
685
|
+
}
|
|
686
|
+
case "field": {
|
|
687
|
+
out.u8(1)
|
|
688
|
+
out.u16le(statement.weight.field)
|
|
689
|
+
break
|
|
690
|
+
}
|
|
691
|
+
case "duration": {
|
|
692
|
+
out.u8(2)
|
|
693
|
+
out.u16le(statement.weight.field)
|
|
694
|
+
break
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
out.u64le(statement.lo)
|
|
698
|
+
switch (statement.hi.kind) {
|
|
699
|
+
case "unbounded": {
|
|
700
|
+
out.u8(0)
|
|
701
|
+
break
|
|
702
|
+
}
|
|
703
|
+
case "lit": {
|
|
704
|
+
out.u8(1)
|
|
705
|
+
out.u8(0)
|
|
706
|
+
out.u64le(statement.hi.value)
|
|
707
|
+
break
|
|
708
|
+
}
|
|
709
|
+
case "targetField": {
|
|
710
|
+
out.u8(1)
|
|
711
|
+
out.u8(1)
|
|
712
|
+
out.u16le(statement.hi.field)
|
|
713
|
+
break
|
|
714
|
+
}
|
|
715
|
+
case "targetDuration": {
|
|
716
|
+
out.u8(1)
|
|
717
|
+
out.u8(2)
|
|
718
|
+
out.u16le(statement.hi.field)
|
|
719
|
+
break
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
putSide(out, relations, statement.source)
|
|
723
|
+
break
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return new Uint8Array(internalBlake3(out.finish()))
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
export type {
|
|
731
|
+
FieldInfo,
|
|
732
|
+
HiInfo,
|
|
733
|
+
LogDescriptor,
|
|
734
|
+
LogTheory,
|
|
735
|
+
RelationInfo,
|
|
736
|
+
SerialStatement,
|
|
737
|
+
SideInfo,
|
|
738
|
+
StatementInfo,
|
|
739
|
+
WeightInfo
|
|
740
|
+
}
|
|
741
|
+
export { braidHex, descriptorOf, withFingerprint }
|