@bjornpagen/bumbledb 0.12.2 → 0.14.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 +10 -8
- package/README.md +5 -4
- package/dist/db.d.ts +43 -19
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +70 -65
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/marshal.d.ts +5 -27
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +4 -21
- package/dist/marshal.js.map +1 -1
- package/dist/native.d.ts +37 -20
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js.map +1 -1
- package/dist/query/find.d.ts +14 -9
- package/dist/query/find.d.ts.map +1 -1
- package/dist/query/find.js +2 -2
- package/dist/query/find.js.map +1 -1
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +5 -4
- package/dist/query/lower.js.map +1 -1
- package/dist/query/parse-ir.d.ts.map +1 -1
- package/dist/query/parse-ir.js +11 -9
- package/dist/query/parse-ir.js.map +1 -1
- package/dist/relation.d.ts +4 -25
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +3 -4
- package/dist/relation.js.map +1 -1
- package/package.json +2 -2
- package/src/db.ts +123 -104
- package/src/index.ts +3 -2
- package/src/marshal.ts +5 -35
- package/src/native.ts +34 -16
- package/src/query/find.ts +19 -14
- package/src/query/lower.ts +7 -6
- package/src/query/parse-ir.ts +11 -9
- package/src/relation.ts +3 -28
package/src/db.ts
CHANGED
|
@@ -37,17 +37,7 @@ import type { Exhumed } from "#exhume.ts"
|
|
|
37
37
|
import { exhumeStore } from "#exhume.ts"
|
|
38
38
|
import { rosterOf } from "#fields.ts"
|
|
39
39
|
import { lower } from "#lower.ts"
|
|
40
|
-
import {
|
|
41
|
-
factOf,
|
|
42
|
-
handleOf,
|
|
43
|
-
isFreshField,
|
|
44
|
-
isInserted,
|
|
45
|
-
type KeyFact,
|
|
46
|
-
keyRowOf,
|
|
47
|
-
type Minted,
|
|
48
|
-
recordOf,
|
|
49
|
-
rowOf
|
|
50
|
-
} from "#marshal.ts"
|
|
40
|
+
import { factOf, handleOf, isFreshField, type KeyFact, keyRowOf, recordOf, rowOf } from "#marshal.ts"
|
|
51
41
|
|
|
52
42
|
import type {
|
|
53
43
|
DbHandle,
|
|
@@ -56,6 +46,7 @@ import type {
|
|
|
56
46
|
PreparedHandle,
|
|
57
47
|
SnapshotHandle,
|
|
58
48
|
TxHandle,
|
|
49
|
+
WireFreshRange,
|
|
59
50
|
Violation as WireViolation,
|
|
60
51
|
ViolationFact as WireViolationFact
|
|
61
52
|
} from "#native.ts"
|
|
@@ -65,7 +56,7 @@ import type { Query } from "#query/lower.ts"
|
|
|
65
56
|
import { lowerQuery } from "#query/lower.ts"
|
|
66
57
|
import { decodeAnswers, wireParams } from "#query/run.ts"
|
|
67
58
|
import type { ParamEntry, ParamsRecord } from "#query/scope.ts"
|
|
68
|
-
import type { AnyRelation, Fact,
|
|
59
|
+
import type { AnyRelation, Fact, FreshKeys } from "#relation.ts"
|
|
69
60
|
import type { AnySchema, Schema, SchemaRelation, SchemaRelations } from "#schema.ts"
|
|
70
61
|
import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
71
62
|
|
|
@@ -76,6 +67,70 @@ import { isStatement, type KeyStatement, type Statement } from "#statements.ts"
|
|
|
76
67
|
*/
|
|
77
68
|
type MemberRelation<Rels extends SchemaRelations> = Extract<Rels[keyof Rels], AnyRelation>
|
|
78
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Facts consumed vs facts that changed the in-memory final-state view.
|
|
72
|
+
* The length-1 report is `{ submitted: 1n, changed: 0n | 1n }`.
|
|
73
|
+
*/
|
|
74
|
+
interface MutationReport {
|
|
75
|
+
readonly submitted: bigint
|
|
76
|
+
readonly changed: bigint
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Half-open fresh-id range from one `reserve`. Empty cannot yield a
|
|
81
|
+
* minted id — `start` exists only on the nonempty arm.
|
|
82
|
+
*/
|
|
83
|
+
type FreshRange =
|
|
84
|
+
| {
|
|
85
|
+
readonly empty: true
|
|
86
|
+
readonly count: 0n
|
|
87
|
+
at(index: bigint): undefined
|
|
88
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
89
|
+
}
|
|
90
|
+
| {
|
|
91
|
+
readonly empty: false
|
|
92
|
+
readonly start: bigint
|
|
93
|
+
readonly endExclusive: bigint
|
|
94
|
+
readonly count: bigint
|
|
95
|
+
at(index: bigint): bigint | undefined
|
|
96
|
+
[Symbol.iterator](): IterableIterator<bigint>
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function freshRangeOf(wire: WireFreshRange): FreshRange {
|
|
100
|
+
if (wire.empty) {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
empty: true,
|
|
103
|
+
count: 0n,
|
|
104
|
+
at(_index: bigint) {
|
|
105
|
+
return undefined
|
|
106
|
+
},
|
|
107
|
+
*[Symbol.iterator](): IterableIterator<bigint> {}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
const start = wire.start
|
|
111
|
+
const endExclusive = wire.endExclusive
|
|
112
|
+
const count = endExclusive - start
|
|
113
|
+
return Object.freeze({
|
|
114
|
+
empty: false,
|
|
115
|
+
start,
|
|
116
|
+
endExclusive,
|
|
117
|
+
get count() {
|
|
118
|
+
return count
|
|
119
|
+
},
|
|
120
|
+
at(index: bigint) {
|
|
121
|
+
if (index < 0n || index >= count) {
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
return start + index
|
|
125
|
+
},
|
|
126
|
+
*[Symbol.iterator](): IterableIterator<bigint> {
|
|
127
|
+
for (let id = start; id < endExclusive; id++) {
|
|
128
|
+
yield id
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
79
134
|
/**
|
|
80
135
|
* The key object of a key-statement-selected `get`: exactly the selected
|
|
81
136
|
* `key()` statement's projection fields, each at the relation's own BARE
|
|
@@ -297,21 +352,22 @@ function abandonedOutcome<Rels extends SchemaRelations, R>(
|
|
|
297
352
|
*/
|
|
298
353
|
interface Tx<Rels extends SchemaRelations> {
|
|
299
354
|
/**
|
|
300
|
-
* Records
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
|
|
310
|
-
|
|
355
|
+
* Records a collection of inserts. Singleton is `[fact]`. Empty is
|
|
356
|
+
* lawful. Returns how many facts were consumed and how many changed
|
|
357
|
+
* the in-memory final-state view. Every fact is complete — omitted
|
|
358
|
+
* fresh cells are a type error; mint first with {@link Tx.reserve}.
|
|
359
|
+
*/
|
|
360
|
+
insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
361
|
+
/**
|
|
362
|
+
* Records a collection of deletes. Singleton is `[fact]`. Returns
|
|
363
|
+
* how many facts were consumed and how many changed the view.
|
|
364
|
+
*/
|
|
365
|
+
delete<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport
|
|
366
|
+
/**
|
|
367
|
+
* Mints `count` consecutive fresh values for a `.fresh` field.
|
|
368
|
+
* `count === 0n` is empty and does not yield a start.
|
|
311
369
|
*/
|
|
312
|
-
|
|
313
|
-
/** Records one delete; `true` iff the final state changed. */
|
|
314
|
-
delete<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
370
|
+
reserve<R extends MemberRelation<Rels>>(relation: R, field: FreshKeys<R> & string, count: bigint): FreshRange
|
|
315
371
|
/** Final-state membership of one complete fact. */
|
|
316
372
|
contains<R extends MemberRelation<Rels>>(relation: R, fact: Fact<R>): boolean
|
|
317
373
|
/**
|
|
@@ -791,44 +847,6 @@ const ErrGenerationMoved = errors.new(
|
|
|
791
847
|
"bumbledb generationMoved: a state-changing commit landed since the witness snapshot"
|
|
792
848
|
)
|
|
793
849
|
|
|
794
|
-
/**
|
|
795
|
-
* Fills one insert's omitted fresh cells through the engine's
|
|
796
|
-
* alloc-then-insert dyn lane (there is no insert-with-omitted-fields wire
|
|
797
|
-
* spelling) and collects every fresh cell — minted or resupplied — for the
|
|
798
|
-
* insert's return. Mutates `values` in place with the minted cells.
|
|
799
|
-
*/
|
|
800
|
-
function mintFreshCells(
|
|
801
|
-
txHandle: TxHandle,
|
|
802
|
-
entry: RelationEntry,
|
|
803
|
-
relation: AnyRelation,
|
|
804
|
-
values: Record<string, unknown>
|
|
805
|
-
): Record<string, FactValue> {
|
|
806
|
-
const fresh: Record<string, FactValue> = {}
|
|
807
|
-
for (const declared of relation.data.fields) {
|
|
808
|
-
if (!isFreshField(declared.field)) {
|
|
809
|
-
continue
|
|
810
|
-
}
|
|
811
|
-
let cell = values[declared.name]
|
|
812
|
-
if (cell === undefined) {
|
|
813
|
-
const fieldId = entry.fieldIds.get(declared.name)
|
|
814
|
-
if (fieldId === undefined) {
|
|
815
|
-
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${declared.name}`)
|
|
816
|
-
}
|
|
817
|
-
cell = bridged("bumbledb tx alloc", function mint() {
|
|
818
|
-
return native.txAlloc(txHandle, entry.id, fieldId)
|
|
819
|
-
})
|
|
820
|
-
values[declared.name] = cell
|
|
821
|
-
}
|
|
822
|
-
if (typeof cell !== "bigint") {
|
|
823
|
-
throw errors.new(
|
|
824
|
-
`relation ${relation.name} field ${declared.name}: a fresh cell is a u64 bigint, got ${typeof cell}`
|
|
825
|
-
)
|
|
826
|
-
}
|
|
827
|
-
fresh[declared.name] = cell
|
|
828
|
-
}
|
|
829
|
-
return fresh
|
|
830
|
-
}
|
|
831
|
-
|
|
832
850
|
/**
|
|
833
851
|
* Constructs one open `Db` over an already-admitted handle: builds the
|
|
834
852
|
* id-resolution tables once and closes over them — the `Db` owns handle
|
|
@@ -1215,38 +1233,59 @@ function openDb<Rels extends SchemaRelations>(handle: DbHandle, theory: Schema<R
|
|
|
1215
1233
|
})
|
|
1216
1234
|
}
|
|
1217
1235
|
})
|
|
1218
|
-
function insert<R extends MemberRelation<Rels>>(
|
|
1219
|
-
relation: R,
|
|
1220
|
-
fact: InsertFact<R>
|
|
1221
|
-
): { readonly changed: boolean } & Minted<R> {
|
|
1236
|
+
function insert<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1222
1237
|
assertLive()
|
|
1238
|
+
const rows: FactValue[][] = []
|
|
1239
|
+
for (const fact of facts) {
|
|
1240
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1241
|
+
}
|
|
1223
1242
|
const entry = resolveOrdinary(relation)
|
|
1224
1243
|
const txHandle = resolveTx()
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
const fresh = mintFreshCells(txHandle, entry, relation, values)
|
|
1228
|
-
const row = rowOf(relation.data, values)
|
|
1229
|
-
const changed = bridged("bumbledb tx insert", function record() {
|
|
1230
|
-
return native.txInsert(txHandle, entry.id, row)
|
|
1244
|
+
const report = bridged("bumbledb tx insert", function record() {
|
|
1245
|
+
return native.txInsert(txHandle, entry.id, rows)
|
|
1231
1246
|
})
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1247
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1248
|
+
}
|
|
1249
|
+
function remove<R extends MemberRelation<Rels>>(relation: R, facts: Iterable<Fact<R>>): MutationReport {
|
|
1250
|
+
assertLive()
|
|
1251
|
+
const rows: FactValue[][] = []
|
|
1252
|
+
for (const fact of facts) {
|
|
1253
|
+
rows.push(rowOf(relation.data, recordOf(fact)))
|
|
1235
1254
|
}
|
|
1236
|
-
|
|
1255
|
+
const entry = resolveOrdinary(relation)
|
|
1256
|
+
const txHandle = resolveTx()
|
|
1257
|
+
const report = bridged("bumbledb tx delete", function record() {
|
|
1258
|
+
return native.txDelete(txHandle, entry.id, rows)
|
|
1259
|
+
})
|
|
1260
|
+
return Object.freeze({ submitted: report.submitted, changed: report.changed })
|
|
1237
1261
|
}
|
|
1238
|
-
function
|
|
1262
|
+
function reserve<R extends MemberRelation<Rels>>(
|
|
1263
|
+
relation: R,
|
|
1264
|
+
field: FreshKeys<R> & string,
|
|
1265
|
+
count: bigint
|
|
1266
|
+
): FreshRange {
|
|
1239
1267
|
assertLive()
|
|
1240
1268
|
const entry = resolveOrdinary(relation)
|
|
1269
|
+
const declared = relation.data.fields.find(function byName(candidate) {
|
|
1270
|
+
return candidate.name === field
|
|
1271
|
+
})
|
|
1272
|
+
if (declared === undefined || !isFreshField(declared.field)) {
|
|
1273
|
+
throw errors.new(`relation ${relation.name}: field ${field} is not a fresh cell`)
|
|
1274
|
+
}
|
|
1275
|
+
const fieldId = entry.fieldIds.get(field)
|
|
1276
|
+
if (fieldId === undefined) {
|
|
1277
|
+
throw errors.new(`bumbledb manifest drift: relation ${relation.name} has no field id for ${field}`)
|
|
1278
|
+
}
|
|
1241
1279
|
const txHandle = resolveTx()
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
return native.txDelete(txHandle, entry.id, row)
|
|
1280
|
+
const range = bridged("bumbledb tx reserve", function mint() {
|
|
1281
|
+
return native.txReserve(txHandle, entry.id, fieldId, count)
|
|
1245
1282
|
})
|
|
1283
|
+
return freshRangeOf(range)
|
|
1246
1284
|
}
|
|
1247
1285
|
const tx: Tx<Rels> = Object.freeze({
|
|
1248
1286
|
insert,
|
|
1249
1287
|
delete: remove,
|
|
1288
|
+
reserve,
|
|
1250
1289
|
contains: reads.contains,
|
|
1251
1290
|
get: reads.get
|
|
1252
1291
|
})
|
|
@@ -1418,27 +1457,6 @@ const ErrNewtypeMismatch = errors.new(
|
|
|
1418
1457
|
"bumbledb newtypeMismatch: a statement pairs faces whose newtypes disagree — the faces of a dependency agree on their newtype, or neither carries one"
|
|
1419
1458
|
)
|
|
1420
1459
|
|
|
1421
|
-
/**
|
|
1422
|
-
* `Tx.insert` returns the flattened `{ changed, ...fresh }` record (R11),
|
|
1423
|
-
* where the spread wins: a FRESH field literally named `changed` would
|
|
1424
|
-
* shadow the engine's changed-state report on every insert of its
|
|
1425
|
-
* relation. No field name is reserved SILENTLY — the one unspeakable
|
|
1426
|
-
* spelling is refused here at admission, before any store is touched.
|
|
1427
|
-
* Supplied (non-fresh) fields named `changed` never enter the return
|
|
1428
|
-
* record and stay legal.
|
|
1429
|
-
*/
|
|
1430
|
-
function refuseShadowedChanged(theory: AnySchema): void {
|
|
1431
|
-
for (const [name, member] of Object.entries(theory.relations)) {
|
|
1432
|
-
for (const declared of sealedFieldsOf(member)) {
|
|
1433
|
-
if (declared.name === "changed" && isFreshField(declared.field)) {
|
|
1434
|
-
throw errors.new(
|
|
1435
|
-
`relation ${name}: a fresh field named "changed" would shadow tx.insert's changed-state report in its { changed, ...fresh } return (R11) — rename the fresh field; a supplied field named "changed" stays legal (only fresh cells ride the return)`
|
|
1436
|
-
)
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
1460
|
/**
|
|
1443
1461
|
* The one admission path both verbs share: lower the theory, run one
|
|
1444
1462
|
* bridge call, and wrap the domain refusals — `schemaError` (spec
|
|
@@ -1455,7 +1473,6 @@ function admit<Rels extends SchemaRelations>(
|
|
|
1455
1473
|
storePath: string,
|
|
1456
1474
|
theory: Schema<Rels>
|
|
1457
1475
|
): Db<Rels> {
|
|
1458
|
-
refuseShadowedChanged(theory)
|
|
1459
1476
|
const canonical = path.resolve(storePath)
|
|
1460
1477
|
const spec = lower(theory)
|
|
1461
1478
|
const opened = bridged(`${verb} bumbledb store at ${canonical}`, function callBridge() {
|
|
@@ -1527,9 +1544,11 @@ export type {
|
|
|
1527
1544
|
DeclaredKeyFact,
|
|
1528
1545
|
DeclaredKeyViolation,
|
|
1529
1546
|
DeltaBuild,
|
|
1547
|
+
FreshRange,
|
|
1530
1548
|
ImpliedKeyViolation,
|
|
1531
1549
|
MemberRelation,
|
|
1532
1550
|
MirrorViolation,
|
|
1551
|
+
MutationReport,
|
|
1533
1552
|
OffendingFact,
|
|
1534
1553
|
Prepared,
|
|
1535
1554
|
ReadScope,
|
package/src/index.ts
CHANGED
|
@@ -56,9 +56,11 @@ export type {
|
|
|
56
56
|
DeclaredKeyFact,
|
|
57
57
|
DeclaredKeyViolation,
|
|
58
58
|
DeltaBuild,
|
|
59
|
+
FreshRange,
|
|
59
60
|
ImpliedKeyViolation,
|
|
60
61
|
MemberRelation,
|
|
61
62
|
MirrorViolation,
|
|
63
|
+
MutationReport,
|
|
62
64
|
OffendingFact,
|
|
63
65
|
Prepared,
|
|
64
66
|
ReadScope,
|
|
@@ -114,7 +116,7 @@ export type {
|
|
|
114
116
|
export { bool, bytes, i64, interval, span, str, u64 } from "#fields.ts"
|
|
115
117
|
export type { ClassesOf, ClassWall, LawfulStatements, RelationClasses, SchemaClasses } from "#law.ts"
|
|
116
118
|
export { lower, lowerClosed, lowerRelation } from "#lower.ts"
|
|
117
|
-
export type { KeyFact
|
|
119
|
+
export type { KeyFact } from "#marshal.ts"
|
|
118
120
|
export type { FactValue, ParsedQuery, QueryIr, StatementKindTag } from "#native.ts"
|
|
119
121
|
|
|
120
122
|
export type {
|
|
@@ -169,7 +171,6 @@ export type {
|
|
|
169
171
|
Fact,
|
|
170
172
|
FieldsShape,
|
|
171
173
|
FreshKeys,
|
|
172
|
-
InsertFact,
|
|
173
174
|
Relation,
|
|
174
175
|
RelationData,
|
|
175
176
|
RelationField,
|
package/src/marshal.ts
CHANGED
|
@@ -43,13 +43,6 @@ function isFreshField(field: AnyField): boolean {
|
|
|
43
43
|
return "fresh" in field && field.fresh === true
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* The inferred object type `tx.insert` returns: one property per
|
|
48
|
-
* fresh-marked field of `R`, carrying the minted (or resupplied) id as a
|
|
49
|
-
* bare `bigint`. A relation with no fresh field returns the empty object.
|
|
50
|
-
*/
|
|
51
|
-
type Minted<R extends AnyRelation> = { [K in FreshKeys<R>]: Fact<R>[K] }
|
|
52
|
-
|
|
53
46
|
/**
|
|
54
47
|
* The key object `get` reads through. THE PRIMARY-KEY RULE: `get` always
|
|
55
48
|
* reads through the PRIMARY candidate key — the first-declared one in the
|
|
@@ -74,10 +67,7 @@ type KeyFact<R extends AnyRelation> = [FreshKeys<R>] extends [never]
|
|
|
74
67
|
* ALLOCATION-FREE IDENTITY (the admission predicate is the type
|
|
75
68
|
* reprojection; the value passes through untouched): every consumer
|
|
76
69
|
* downstream — `rowOf`, `keyRowOf`, the query param marshal — only READS
|
|
77
|
-
* properties, so no copy is warranted.
|
|
78
|
-
* (`mintFreshCells` on the insert path) takes its own spread copy at the
|
|
79
|
-
* call site, so the caller's fact object is never written through this
|
|
80
|
-
* seam.
|
|
70
|
+
* properties, so no copy is warranted.
|
|
81
71
|
*/
|
|
82
72
|
function recordOf(fact: object): Readonly<Record<string, unknown>> {
|
|
83
73
|
if (!isStringIndexed(fact)) {
|
|
@@ -203,9 +193,8 @@ function cellOf(context: string, field: AnyField, value: unknown): FactValue {
|
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
195
|
* Marshals one complete fact object to its positional row, in field
|
|
206
|
-
* declaration order (= ordinal ids). Every declared field must be present
|
|
207
|
-
*
|
|
208
|
-
* fresh cells via the engine's alloc lane).
|
|
196
|
+
* declaration order (= ordinal ids). Every declared field must be present.
|
|
197
|
+
* Mint with `tx.reserve` first; insert takes complete facts.
|
|
209
198
|
*/
|
|
210
199
|
function rowOf(relation: RelationData, fact: Readonly<Record<string, unknown>>): FactValue[] {
|
|
211
200
|
return relation.fields.map(function marshalCell(declared) {
|
|
@@ -260,25 +249,6 @@ function isCompleteFact<R extends AnyRelation>(
|
|
|
260
249
|
})
|
|
261
250
|
}
|
|
262
251
|
|
|
263
|
-
/**
|
|
264
|
-
* The insert-return trusted seam (R11): one insert's return carries the
|
|
265
|
-
* engine's changed-state report beside the collected fresh cells (minted by
|
|
266
|
-
* the engine or resupplied by the caller) — the bit is verified boolean and
|
|
267
|
-
* the fresh ids present, same presence-only direction as
|
|
268
|
-
* {@link isCompleteFact}.
|
|
269
|
-
*/
|
|
270
|
-
function isInserted<R extends AnyRelation>(
|
|
271
|
-
relation: R,
|
|
272
|
-
value: Readonly<Record<string, FactValue | boolean>>
|
|
273
|
-
): value is Readonly<Record<string, FactValue | boolean>> & { readonly changed: boolean } & Minted<R> {
|
|
274
|
-
return (
|
|
275
|
-
typeof value.changed === "boolean" &&
|
|
276
|
-
relation.data.fields.every(function presentWhenFresh(declared) {
|
|
277
|
-
return !isFreshField(declared.field) || value[declared.name] !== undefined
|
|
278
|
-
})
|
|
279
|
-
)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
252
|
/**
|
|
283
253
|
* Unmarshals one positional row to the relation's named, frozen fact object
|
|
284
254
|
* of bare structural values — the inverse of {@link rowOf},
|
|
@@ -310,5 +280,5 @@ function factOf<R extends AnyRelation>(relation: R, row: readonly FactValue[]):
|
|
|
310
280
|
return decoded
|
|
311
281
|
}
|
|
312
282
|
|
|
313
|
-
export type { KeyFact
|
|
314
|
-
export { cellOf, factOf, handleOf, isFreshField,
|
|
283
|
+
export type { KeyFact }
|
|
284
|
+
export { cellOf, factOf, handleOf, isFreshField, keyRowOf, recordOf, rowOf }
|
package/src/native.ts
CHANGED
|
@@ -44,6 +44,24 @@ type TxHandle = { readonly __brand: "bumbledb.tx" }
|
|
|
44
44
|
/** One prepared query (plan pinned at prepare). */
|
|
45
45
|
type PreparedHandle = { readonly __brand: "bumbledb.prepared" }
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Engine mutation report as it crosses napi: both counts are engine
|
|
49
|
+
* values, never reconstructed from JS length.
|
|
50
|
+
*/
|
|
51
|
+
interface WireMutationReport {
|
|
52
|
+
readonly submitted: bigint
|
|
53
|
+
readonly changed: bigint
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Engine fresh-id range as it crosses napi. Empty cannot yield a start —
|
|
58
|
+
* `start` is a minted id only on the nonempty arm. (C wires empty as
|
|
59
|
+
* `{ start: 0, end_exclusive: 0 }` at that boundary only.)
|
|
60
|
+
*/
|
|
61
|
+
type WireFreshRange =
|
|
62
|
+
| { readonly empty: true }
|
|
63
|
+
| { readonly empty: false; readonly start: bigint; readonly endExclusive: bigint }
|
|
64
|
+
|
|
47
65
|
/** A half-open interval `[start, end)` as it crosses the boundary. */
|
|
48
66
|
interface IntervalValue {
|
|
49
67
|
readonly start: bigint
|
|
@@ -118,15 +136,14 @@ interface RuleIr {
|
|
|
118
136
|
readonly conditions: readonly ConditionTreeIr[]
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
/** One find term (mirrors `ir::FindTerm`). Count
|
|
139
|
+
/** One find term (mirrors `ir::FindTerm`). Count is nullary; pack and folds carry `over`. */
|
|
122
140
|
type FoldOpIr = { readonly kind: "sum" } | { readonly kind: "min" } | { readonly kind: "max" }
|
|
123
141
|
|
|
124
|
-
type ArgOpIr = FoldOpIr | { readonly kind: "pack" }
|
|
125
|
-
|
|
126
142
|
type FindTermIr =
|
|
127
143
|
| { readonly kind: "var"; readonly var: number }
|
|
128
|
-
| { readonly kind: "
|
|
129
|
-
| { readonly kind: "aggregate"; readonly op:
|
|
144
|
+
| { readonly kind: "count" }
|
|
145
|
+
| { readonly kind: "aggregate"; readonly op: FoldOpIr; readonly over: number }
|
|
146
|
+
| { readonly kind: "pack"; readonly over: number }
|
|
130
147
|
| { readonly kind: "measure"; readonly var: number }
|
|
131
148
|
| { readonly kind: "aggregateMeasure"; readonly op: FoldOpIr; readonly over: number }
|
|
132
149
|
|
|
@@ -497,12 +514,14 @@ interface Native {
|
|
|
497
514
|
*/
|
|
498
515
|
dbWriteFrom(db: DbHandle, snap: SnapshotHandle): WriteFromResult
|
|
499
516
|
/**
|
|
500
|
-
* Records
|
|
501
|
-
*
|
|
517
|
+
* Records a collection of inserts into the delta; returns the engine
|
|
518
|
+
* `{ submitted, changed }` report. `rows` is an array of value-arrays
|
|
519
|
+
* in sealed field order. Empty is lawful and still a mutation (poison
|
|
520
|
+
* is observed). Nothing is judged until commit; shape violations throw typed.
|
|
502
521
|
*/
|
|
503
|
-
txInsert(tx: TxHandle, relationId: number,
|
|
504
|
-
/** Records a
|
|
505
|
-
txDelete(tx: TxHandle, relationId: number,
|
|
522
|
+
txInsert(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
|
|
523
|
+
/** Records a collection of deletes; returns the engine `{ submitted, changed }` report. */
|
|
524
|
+
txDelete(tx: TxHandle, relationId: number, rows: readonly (readonly FactValue[])[]): WireMutationReport
|
|
506
525
|
/**
|
|
507
526
|
* Final-state membership (base + pending delta — the exact view the
|
|
508
527
|
* commit judgment judges; check-then-act is race-free by construction).
|
|
@@ -511,12 +530,10 @@ interface Native {
|
|
|
511
530
|
/** Final-state point lookup through a key statement; `null` on a miss. */
|
|
512
531
|
txGet(tx: TxHandle, relationId: number, keyStatementId: number, keyValues: readonly FactValue[]): FactValue[] | null
|
|
513
532
|
/**
|
|
514
|
-
* Mints
|
|
515
|
-
*
|
|
516
|
-
* insert-with-omitted-fields spelling; include the minted id in the
|
|
517
|
-
* full row).
|
|
533
|
+
* Mints `count` consecutive fresh values for `(relationId, fieldId)`.
|
|
534
|
+
* `count === 0n` is empty and does not yield a start.
|
|
518
535
|
*/
|
|
519
|
-
|
|
536
|
+
txReserve(tx: TxHandle, relationId: number, fieldId: number, count: bigint): WireFreshRange
|
|
520
537
|
/**
|
|
521
538
|
* Commits the delta: every dependency statement judged against the
|
|
522
539
|
* final state; a rejection carries the complete violation rendering.
|
|
@@ -641,7 +658,6 @@ function bridged<T>(context: string, run: () => T): T {
|
|
|
641
658
|
|
|
642
659
|
export type {
|
|
643
660
|
AggOpIr,
|
|
644
|
-
ArgOpIr,
|
|
645
661
|
AtomIr,
|
|
646
662
|
AtomSourceIr,
|
|
647
663
|
CmpOpIr,
|
|
@@ -683,6 +699,8 @@ export type {
|
|
|
683
699
|
TxHandle,
|
|
684
700
|
Violation,
|
|
685
701
|
ViolationFact,
|
|
702
|
+
WireFreshRange,
|
|
703
|
+
WireMutationReport,
|
|
686
704
|
WriteFromResult
|
|
687
705
|
}
|
|
688
706
|
export { bridged, loadNativeBinding, native, SHIPPED_PLATFORMS }
|
package/src/query/find.ts
CHANGED
|
@@ -24,19 +24,25 @@ import type { IntervalVarOk, NumericVarOk, OrderVarOk } from "#query/atom.ts"
|
|
|
24
24
|
import type { AnyVar, Duration, MintSlotOf } from "#query/scope.ts"
|
|
25
25
|
|
|
26
26
|
/** One aggregate operator name of the find vocabulary. */
|
|
27
|
-
type
|
|
27
|
+
type FoldOpName = "sum" | "min" | "max" | "pack"
|
|
28
|
+
type AggOpName = "count" | FoldOpName
|
|
29
|
+
|
|
30
|
+
/** Nullary count: no `over` exists to inhabit. */
|
|
31
|
+
interface CountAgg {
|
|
32
|
+
readonly agg: "count"
|
|
33
|
+
}
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
|
-
* One aggregate
|
|
31
|
-
* BY REFERENCE.
|
|
36
|
+
* One fold aggregate: the op and the variable (or measure) it folds
|
|
37
|
+
* BY REFERENCE. Count is [`CountAgg`], not this type with `undefined`.
|
|
32
38
|
*/
|
|
33
|
-
interface Agg<Op extends
|
|
39
|
+
interface Agg<Op extends FoldOpName, Over extends AnyVar | Duration> {
|
|
34
40
|
readonly agg: Op
|
|
35
41
|
readonly over: Over
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
/** Any aggregate find value. */
|
|
39
|
-
type AnyAgg = Agg<
|
|
45
|
+
type AnyAgg = CountAgg | Agg<FoldOpName, AnyVar | Duration>
|
|
40
46
|
|
|
41
47
|
/** One find entry: a projected variable, the measure, or an aggregate. */
|
|
42
48
|
type FindEntry = AnyVar | Duration | AnyAgg
|
|
@@ -44,17 +50,14 @@ type FindEntry = AnyVar | Duration | AnyAgg
|
|
|
44
50
|
/** The `find` record: column name → find entry. Keys ARE the answer columns. */
|
|
45
51
|
type FindShape = Readonly<Record<string, FindEntry>>
|
|
46
52
|
|
|
47
|
-
/** Builds one aggregate value. */
|
|
48
|
-
function aggregate<Op extends
|
|
49
|
-
op: Op,
|
|
50
|
-
over: Over
|
|
51
|
-
): Agg<Op, Over> {
|
|
53
|
+
/** Builds one fold aggregate value. */
|
|
54
|
+
function aggregate<Op extends FoldOpName, Over extends AnyVar | Duration>(op: Op, over: Over): Agg<Op, Over> {
|
|
52
55
|
return Object.freeze({ agg: op, over })
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
/** Nullary count: |the group's set of distinct full bindings|, `bigint`. */
|
|
56
|
-
function count():
|
|
57
|
-
return
|
|
59
|
+
function count(): CountAgg {
|
|
60
|
+
return Object.freeze({ agg: "count" })
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -119,7 +122,7 @@ type FindEntryOk<E> = E extends AnyVar
|
|
|
119
122
|
? true
|
|
120
123
|
: E extends Duration<infer V extends AnyVar>
|
|
121
124
|
? IntervalVarOk<V>
|
|
122
|
-
: E extends
|
|
125
|
+
: E extends CountAgg
|
|
123
126
|
? true
|
|
124
127
|
: E extends Agg<"sum", infer O>
|
|
125
128
|
? SumOverOk<O>
|
|
@@ -151,7 +154,7 @@ type FindValue<E> = E extends AnyVar
|
|
|
151
154
|
? Infer<E["field"]>
|
|
152
155
|
: E extends Duration<AnyVar>
|
|
153
156
|
? bigint
|
|
154
|
-
: E extends
|
|
157
|
+
: E extends CountAgg
|
|
155
158
|
? bigint
|
|
156
159
|
: E extends Agg<"sum" | "min" | "max", infer O>
|
|
157
160
|
? O extends AnyVar
|
|
@@ -179,10 +182,12 @@ export type {
|
|
|
179
182
|
AnyAgg,
|
|
180
183
|
CheckFind,
|
|
181
184
|
CheckRecFind,
|
|
185
|
+
CountAgg,
|
|
182
186
|
FindEntry,
|
|
183
187
|
FindEntryOk,
|
|
184
188
|
FindShape,
|
|
185
189
|
FindValue,
|
|
190
|
+
FoldOpName,
|
|
186
191
|
HeadRecordOf,
|
|
187
192
|
RowOfFind
|
|
188
193
|
}
|
package/src/query/lower.ts
CHANGED
|
@@ -772,7 +772,7 @@ function advanceInterior(
|
|
|
772
772
|
}
|
|
773
773
|
|
|
774
774
|
/** Narrows a find entry to an aggregate value. */
|
|
775
|
-
function isAggregateEntry(value: unknown): value is { readonly agg: string; readonly over
|
|
775
|
+
function isAggregateEntry(value: unknown): value is { readonly agg: string; readonly over?: unknown } {
|
|
776
776
|
return typeof value === "object" && value !== null && "agg" in value
|
|
777
777
|
}
|
|
778
778
|
|
|
@@ -785,11 +785,12 @@ function asVarTerm(context: string, value: unknown): AnyVar {
|
|
|
785
785
|
}
|
|
786
786
|
|
|
787
787
|
/** Classifies one aggregate find entry into its runtime data (variables ride by reference). */
|
|
788
|
-
function aggDataOf(name: string, entry: { readonly agg: string; readonly over
|
|
788
|
+
function aggDataOf(name: string, entry: { readonly agg: string; readonly over?: unknown }): AggData {
|
|
789
|
+
if (entry.agg === "count") {
|
|
790
|
+
return Object.freeze({ op: "count" as const })
|
|
791
|
+
}
|
|
789
792
|
const over = entry.over
|
|
790
793
|
switch (entry.agg) {
|
|
791
|
-
case "count":
|
|
792
|
-
return Object.freeze({ op: "count" as const })
|
|
793
794
|
case "sum":
|
|
794
795
|
case "min":
|
|
795
796
|
case "max": {
|
|
@@ -2051,7 +2052,7 @@ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
|
|
|
2051
2052
|
const agg = entry.agg
|
|
2052
2053
|
switch (agg.op) {
|
|
2053
2054
|
case "count":
|
|
2054
|
-
return { kind: "
|
|
2055
|
+
return { kind: "count" }
|
|
2055
2056
|
case "fold": {
|
|
2056
2057
|
if ("duration" in agg.over) {
|
|
2057
2058
|
return { kind: "aggregateMeasure", op: { kind: agg.fold }, over: ids.of(agg.over.duration) }
|
|
@@ -2059,7 +2060,7 @@ function lowerFind(entry: FindEntryData, ids: VarIds): FindTermIr {
|
|
|
2059
2060
|
return { kind: "aggregate", op: { kind: agg.fold }, over: ids.of(agg.over) }
|
|
2060
2061
|
}
|
|
2061
2062
|
case "pack":
|
|
2062
|
-
return { kind: "
|
|
2063
|
+
return { kind: "pack", over: ids.of(agg.over) }
|
|
2063
2064
|
}
|
|
2064
2065
|
}
|
|
2065
2066
|
|
package/src/query/parse-ir.ts
CHANGED
|
@@ -50,28 +50,30 @@ function align(context: string, head: readonly HeadTermIr[], rules: readonly Rul
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** Count
|
|
53
|
+
/** Count is nullary; pack and folds require `over`. */
|
|
54
54
|
function parseFind(context: string, find: FindTermIr): void {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (find.op.kind === "count") {
|
|
59
|
-
if ("over" in find) {
|
|
55
|
+
const raw = find as Record<string, unknown>
|
|
56
|
+
if (find.kind === "count") {
|
|
57
|
+
if ("over" in raw) {
|
|
60
58
|
throw errors.new(`${context}: Count carries no over`)
|
|
61
59
|
}
|
|
62
60
|
return
|
|
63
61
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
62
|
+
if (find.kind === "pack" || find.kind === "aggregate" || find.kind === "aggregateMeasure") {
|
|
63
|
+
if (!("over" in raw)) {
|
|
64
|
+
throw errors.new(`${context}: ${find.kind} requires over`)
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Head family of one find term: measure is a var slot;
|
|
69
|
+
/** Head family of one find term: measure is a var slot; count/pack/folds are aggregates. */
|
|
70
70
|
function findFamily(find: FindTermIr): "var" | "aggregate" {
|
|
71
71
|
switch (find.kind) {
|
|
72
72
|
case "var":
|
|
73
73
|
case "measure":
|
|
74
74
|
return "var"
|
|
75
|
+
case "count":
|
|
76
|
+
case "pack":
|
|
75
77
|
case "aggregate":
|
|
76
78
|
case "aggregateMeasure":
|
|
77
79
|
return "aggregate"
|