@bjornpagen/bumbledb 0.3.0 → 0.4.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 +66 -46
- package/README.md +28 -15
- package/dist/closed.d.ts +50 -73
- package/dist/closed.d.ts.map +1 -1
- package/dist/closed.js +38 -109
- package/dist/closed.js.map +1 -1
- package/dist/db.d.ts +4 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +26 -3
- package/dist/db.js.map +1 -1
- package/dist/face.d.ts +39 -39
- package/dist/face.d.ts.map +1 -1
- package/dist/face.js +7 -16
- package/dist/face.js.map +1 -1
- package/dist/fields.d.ts +28 -14
- package/dist/fields.d.ts.map +1 -1
- package/dist/fields.js +15 -14
- package/dist/fields.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/marshal.d.ts +33 -6
- package/dist/marshal.d.ts.map +1 -1
- package/dist/marshal.js +67 -6
- package/dist/marshal.js.map +1 -1
- package/dist/query/atom.d.ts +59 -14
- package/dist/query/atom.d.ts.map +1 -1
- package/dist/query/atom.js +3 -0
- package/dist/query/atom.js.map +1 -1
- package/dist/query/lower.d.ts.map +1 -1
- package/dist/query/lower.js +251 -26
- package/dist/query/lower.js.map +1 -1
- package/dist/query/run.d.ts +15 -5
- package/dist/query/run.d.ts.map +1 -1
- package/dist/query/run.js +26 -6
- package/dist/query/run.js.map +1 -1
- package/dist/query/scope.d.ts +35 -13
- package/dist/query/scope.d.ts.map +1 -1
- package/dist/query/scope.js +16 -4
- package/dist/query/scope.js.map +1 -1
- package/dist/relation.d.ts +8 -7
- package/dist/relation.d.ts.map +1 -1
- package/dist/relation.js +32 -10
- package/dist/relation.js.map +1 -1
- package/dist/spec.d.ts +3 -2
- package/dist/spec.d.ts.map +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/statements.d.ts +10 -4
- package/dist/statements.d.ts.map +1 -1
- package/dist/statements.js +79 -7
- package/dist/statements.js.map +1 -1
- package/package.json +2 -2
- package/src/closed.ts +72 -179
- package/src/db.ts +42 -5
- package/src/face.ts +34 -45
- package/src/fields.ts +46 -34
- package/src/index.ts +1 -2
- package/src/marshal.ts +74 -7
- package/src/query/atom.ts +58 -15
- package/src/query/lower.ts +301 -28
- package/src/query/run.ts +26 -6
- package/src/query/scope.ts +49 -15
- package/src/relation.ts +45 -17
- package/src/spec.ts +3 -2
- package/src/statements.ts +86 -7
package/src/query/lower.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import * as errors from "@superbuilders/errors"
|
|
29
|
-
import type { AnyField } from "#fields.ts"
|
|
29
|
+
import type { AnyField, ClosedRoster } from "#fields.ts"
|
|
30
30
|
import { assertDeclarationOrderKey } from "#fields.ts"
|
|
31
31
|
import type { ClassRecordOf, SchemaClasses } from "#law.ts"
|
|
32
32
|
import type {
|
|
@@ -495,6 +495,56 @@ function matchFieldsOf(owner: MatchOwner): readonly RelationField[] {
|
|
|
495
495
|
return owner.data.fields
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Judges one membership ARRAY at a binding position — legal exactly at a
|
|
500
|
+
* CLOSED-reference field (the owner ruling: ordinary u64/str membership is
|
|
501
|
+
* spelled through `r.inSet` params; literal arrays are the closed
|
|
502
|
+
* vocabulary's spelling), holding ≥ 2 DISTINCT handle names (the
|
|
503
|
+
* degenerate sets are refusals: empty selects nothing, one element is the
|
|
504
|
+
* bare literal respelled, and a duplicate member is the same respelling in
|
|
505
|
+
* disguise — write each member once). The returned name is
|
|
506
|
+
* CONTENT-ADDRESSED (vocabulary + the member SET — the key sorts a copy,
|
|
507
|
+
* so two spellings of one set, reordered or not, share one dense
|
|
508
|
+
* `ParamId`); the members are shape-checked strings here and
|
|
509
|
+
* roster-verified at the one verification point (`taggedHandleId`) when
|
|
510
|
+
* the SDK supplies the set at execute — the same moment a bound `r.inSet`
|
|
511
|
+
* param's members are judged.
|
|
512
|
+
*/
|
|
513
|
+
function membershipSet(
|
|
514
|
+
context: string,
|
|
515
|
+
field: AnyField,
|
|
516
|
+
value: readonly unknown[]
|
|
517
|
+
): { readonly name: string; readonly members: readonly string[] } {
|
|
518
|
+
if (!("closed" in field)) {
|
|
519
|
+
throw errors.new(
|
|
520
|
+
`${context}: a membership array is the closed-reference spelling — ordinary field membership is a bound ∈-set param (r.inSet)`
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
if (value.length === 0) {
|
|
524
|
+
throw errors.new(`${context}: an empty membership array selects nothing — write the query you mean`)
|
|
525
|
+
}
|
|
526
|
+
if (value.length === 1) {
|
|
527
|
+
throw errors.new(
|
|
528
|
+
`${context}: a one-element membership array is the bare literal respelled — write the literal (the canonical-utterance law: one meaning, one spelling)`
|
|
529
|
+
)
|
|
530
|
+
}
|
|
531
|
+
const seen = new Set<string>()
|
|
532
|
+
const members = value.map(function memberName(member) {
|
|
533
|
+
if (typeof member !== "string") {
|
|
534
|
+
throw literalShapeError(context, `a ${field.closed.name} handle name (string)`, member)
|
|
535
|
+
}
|
|
536
|
+
if (seen.has(member)) {
|
|
537
|
+
throw errors.new(
|
|
538
|
+
`${context}: the membership array spells ${member} twice — write it once (the canonical-utterance law: one meaning, one spelling)`
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
seen.add(member)
|
|
542
|
+
return member
|
|
543
|
+
})
|
|
544
|
+
const key = [...members].sort()
|
|
545
|
+
return { name: `∈ ${field.closed.name} ${JSON.stringify(key)}`, members: Object.freeze(members) }
|
|
546
|
+
}
|
|
547
|
+
|
|
498
548
|
/**
|
|
499
549
|
* Resolves a bindings record against an atom owner's matchable fields (a
|
|
500
550
|
* relation's declared fields; a closed relation's sealed id + columns), in
|
|
@@ -539,14 +589,26 @@ function resolveBindings(
|
|
|
539
589
|
case "param": {
|
|
540
590
|
bound = Object.freeze({ kind: "param" as const, name: value.name })
|
|
541
591
|
uses.push(
|
|
542
|
-
Object.freeze({
|
|
592
|
+
Object.freeze({
|
|
593
|
+
name: value.name,
|
|
594
|
+
shape: "value" as const,
|
|
595
|
+
anchor: declared.field,
|
|
596
|
+
op: "binding" as const,
|
|
597
|
+
members: undefined
|
|
598
|
+
})
|
|
543
599
|
)
|
|
544
600
|
break
|
|
545
601
|
}
|
|
546
602
|
case "setParam": {
|
|
547
603
|
bound = Object.freeze({ kind: "setParam" as const, name: value.name })
|
|
548
604
|
uses.push(
|
|
549
|
-
Object.freeze({
|
|
605
|
+
Object.freeze({
|
|
606
|
+
name: value.name,
|
|
607
|
+
shape: "set" as const,
|
|
608
|
+
anchor: declared.field,
|
|
609
|
+
op: "binding" as const,
|
|
610
|
+
members: undefined
|
|
611
|
+
})
|
|
550
612
|
)
|
|
551
613
|
break
|
|
552
614
|
}
|
|
@@ -559,6 +621,18 @@ function resolveBindings(
|
|
|
559
621
|
`${context}.${fieldName}: the measure is not a field-typed value — it lives in comparisons and select entries`
|
|
560
622
|
)
|
|
561
623
|
}
|
|
624
|
+
} else if (Array.isArray(value)) {
|
|
625
|
+
const set = membershipSet(`${context}.${fieldName}`, declared.field, value)
|
|
626
|
+
bound = Object.freeze({ kind: "literalSet" as const, name: set.name, members: set.members })
|
|
627
|
+
uses.push(
|
|
628
|
+
Object.freeze({
|
|
629
|
+
name: set.name,
|
|
630
|
+
shape: "set" as const,
|
|
631
|
+
anchor: declared.field,
|
|
632
|
+
op: "binding" as const,
|
|
633
|
+
members: set.members
|
|
634
|
+
})
|
|
635
|
+
)
|
|
562
636
|
} else {
|
|
563
637
|
bound = Object.freeze({ kind: "literal" as const, value })
|
|
564
638
|
}
|
|
@@ -651,7 +725,8 @@ function sideUses(
|
|
|
651
725
|
name: side.name,
|
|
652
726
|
shape: side.kind === "param" ? ("value" as const) : ("set" as const),
|
|
653
727
|
anchor,
|
|
654
|
-
op
|
|
728
|
+
op,
|
|
729
|
+
members: undefined
|
|
655
730
|
})
|
|
656
731
|
)
|
|
657
732
|
}
|
|
@@ -671,7 +746,13 @@ function condDataOf(cond: AnyCond, varFields: Readonly<Record<string, ClassedFie
|
|
|
671
746
|
} else if (isTerm(maskValue) && maskValue[term] === "maskParam") {
|
|
672
747
|
mask = Object.freeze({ kind: "param" as const, name: maskValue.name })
|
|
673
748
|
uses.push(
|
|
674
|
-
Object.freeze({
|
|
749
|
+
Object.freeze({
|
|
750
|
+
name: maskValue.name,
|
|
751
|
+
shape: "mask" as const,
|
|
752
|
+
anchor: undefined,
|
|
753
|
+
op: "allen" as const,
|
|
754
|
+
members: undefined
|
|
755
|
+
})
|
|
675
756
|
)
|
|
676
757
|
} else {
|
|
677
758
|
throw errors.new("allen: the mask position takes a 13-bit mask number or a maskParam")
|
|
@@ -742,14 +823,27 @@ function isAggregateEntry(
|
|
|
742
823
|
return typeof value === "object" && value !== null && "agg" in value
|
|
743
824
|
}
|
|
744
825
|
|
|
745
|
-
/**
|
|
826
|
+
/**
|
|
827
|
+
* Classifies one select entry into its named answer column. The `closed`
|
|
828
|
+
* slice is resolved LATER, at rule completion (`completeRule`), where the
|
|
829
|
+
* rule's `varFields` are in hand — until then every column is provisionally
|
|
830
|
+
* bare.
|
|
831
|
+
*/
|
|
746
832
|
function selectColumnOf(entry: unknown): SelectColumn {
|
|
747
833
|
if (typeof entry === "string") {
|
|
748
|
-
return Object.freeze({
|
|
834
|
+
return Object.freeze({
|
|
835
|
+
name: entry,
|
|
836
|
+
entry: Object.freeze({ kind: "var" as const, over: entry }),
|
|
837
|
+
closed: undefined
|
|
838
|
+
})
|
|
749
839
|
}
|
|
750
840
|
if (isTerm(entry)) {
|
|
751
841
|
if (entry[term] === "duration") {
|
|
752
|
-
return Object.freeze({
|
|
842
|
+
return Object.freeze({
|
|
843
|
+
name: entry.name,
|
|
844
|
+
entry: Object.freeze({ kind: "measure" as const, over: entry.name }),
|
|
845
|
+
closed: undefined
|
|
846
|
+
})
|
|
753
847
|
}
|
|
754
848
|
throw errors.new(
|
|
755
849
|
`query select: a ${entry[term]} is not projectable — select takes variable names, duration(v), or aggregates`
|
|
@@ -768,7 +862,11 @@ function aggregateColumnOf(entry: {
|
|
|
768
862
|
readonly key: unknown
|
|
769
863
|
}): SelectColumn {
|
|
770
864
|
function column(name: string, agg: AggData): SelectColumn {
|
|
771
|
-
return Object.freeze({
|
|
865
|
+
return Object.freeze({
|
|
866
|
+
name,
|
|
867
|
+
entry: Object.freeze({ kind: "aggregate" as const, agg: Object.freeze(agg) }),
|
|
868
|
+
closed: undefined
|
|
869
|
+
})
|
|
772
870
|
}
|
|
773
871
|
const over = entry.over
|
|
774
872
|
switch (entry.agg) {
|
|
@@ -809,6 +907,26 @@ function aggregateColumnOf(entry: {
|
|
|
809
907
|
}
|
|
810
908
|
}
|
|
811
909
|
|
|
910
|
+
/**
|
|
911
|
+
* The orderable ban's pointed refusal (`docs/architecture/10-data-model.md`
|
|
912
|
+
* § orderability): a closed reference is equality-and-membership only —
|
|
913
|
+
* its declaration-id order is an encoding accident, so every
|
|
914
|
+
* order-comparison and fold position refuses it. The construction-time
|
|
915
|
+
* twin of the type tier's `OrderVarOk` exclusion, so the wall holds for
|
|
916
|
+
* untyped callers too (the engine cannot backstop this one: the wire IR
|
|
917
|
+
* carries plain u64s, no rosters).
|
|
918
|
+
*/
|
|
919
|
+
function closedOrderError(context: string, position: string, vocabulary: string): Error {
|
|
920
|
+
return errors.new(
|
|
921
|
+
`${context}: ${position} is a ${vocabulary} reference — declaration order is an accident, not semantics: vocabularies do not order (docs/architecture/10-data-model.md; equality, membership, and counting remain)`
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** The comparison ops the orderable ban covers (order roster + point membership — every order-comparison position). */
|
|
926
|
+
function isOrderOp(op: CmpKind | "binding"): op is "lt" | "le" | "gt" | "ge" | "pointIn" {
|
|
927
|
+
return op === "lt" || op === "le" || op === "gt" || op === "ge" || op === "pointIn"
|
|
928
|
+
}
|
|
929
|
+
|
|
812
930
|
/** Requires a var name to be bound by a relation atom of the rule. */
|
|
813
931
|
function assertBound(context: string, varFields: Readonly<Record<string, ClassedField>>, name: string): ClassedField {
|
|
814
932
|
const slot = varFields[name]
|
|
@@ -841,7 +959,10 @@ function validateCond(context: string, varFields: Readonly<Record<string, Classe
|
|
|
841
959
|
if (cond.kind === "cmp") {
|
|
842
960
|
for (const side of [cond.lhs, cond.rhs]) {
|
|
843
961
|
if (side.kind === "var") {
|
|
844
|
-
assertBound(context, varFields, side.name)
|
|
962
|
+
const slot = assertBound(context, varFields, side.name)
|
|
963
|
+
if (isOrderOp(cond.op) && "closed" in slot.field) {
|
|
964
|
+
throw closedOrderError(context, `the ${cond.op} side ${side.name}`, slot.field.closed.name)
|
|
965
|
+
}
|
|
845
966
|
}
|
|
846
967
|
if (side.kind === "measure") {
|
|
847
968
|
assertIntervalBound(context, varFields, side.name)
|
|
@@ -887,22 +1008,69 @@ function validateColumn(
|
|
|
887
1008
|
return
|
|
888
1009
|
case "fold": {
|
|
889
1010
|
if (typeof agg.over === "string") {
|
|
890
|
-
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
1011
|
+
const slot = assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
1012
|
+
if ("closed" in slot.field) {
|
|
1013
|
+
throw closedOrderError(
|
|
1014
|
+
`${context} select ${column.name}`,
|
|
1015
|
+
`the ${agg.fold} input ${agg.over}`,
|
|
1016
|
+
slot.field.closed.name
|
|
1017
|
+
)
|
|
1018
|
+
}
|
|
891
1019
|
return
|
|
892
1020
|
}
|
|
893
1021
|
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over.duration)
|
|
894
1022
|
return
|
|
895
1023
|
}
|
|
896
|
-
case "arg":
|
|
1024
|
+
case "arg": {
|
|
897
1025
|
assertBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
898
|
-
assertBound(`${context} select ${column.name}`, varFields, agg.key)
|
|
1026
|
+
const key = assertBound(`${context} select ${column.name}`, varFields, agg.key)
|
|
1027
|
+
if ("closed" in key.field) {
|
|
1028
|
+
throw closedOrderError(
|
|
1029
|
+
`${context} select ${column.name}`,
|
|
1030
|
+
`the ${agg.direction} key ${agg.key}`,
|
|
1031
|
+
key.field.closed.name
|
|
1032
|
+
)
|
|
1033
|
+
}
|
|
899
1034
|
return
|
|
1035
|
+
}
|
|
900
1036
|
case "pack":
|
|
901
1037
|
assertIntervalBound(`${context} select ${column.name}`, varFields, agg.over)
|
|
902
1038
|
return
|
|
903
1039
|
}
|
|
904
1040
|
}
|
|
905
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Resolves the roster one select column decodes through: a projected var,
|
|
1044
|
+
* or an Arg-carried payload, bound at a closed-referencing field carries
|
|
1045
|
+
* that field's roster (read off `varFields` — the same slot the domain
|
|
1046
|
+
* machinery reads), and `decodeAnswers` lifts the column's row ids back to
|
|
1047
|
+
* handle NAMES through it — the runtime twin of the row type's `Infer`
|
|
1048
|
+
* claim. Every other entry decodes bare: counts are counts, the measure
|
|
1049
|
+
* and `pack` are never closed, and a closed FOLD is banned outright
|
|
1050
|
+
* ({@link closedOrderError}) before this resolution runs.
|
|
1051
|
+
*/
|
|
1052
|
+
function selectClosedOf(
|
|
1053
|
+
varFields: Readonly<Record<string, ClassedField>>,
|
|
1054
|
+
entry: SelectEntryData
|
|
1055
|
+
): ClosedRoster | undefined {
|
|
1056
|
+
let over: string | undefined
|
|
1057
|
+
if (entry.kind === "var") {
|
|
1058
|
+
over = entry.over
|
|
1059
|
+
} else if (entry.kind === "aggregate" && entry.agg.op === "arg") {
|
|
1060
|
+
over = entry.agg.over
|
|
1061
|
+
} else {
|
|
1062
|
+
over = undefined
|
|
1063
|
+
}
|
|
1064
|
+
if (over === undefined) {
|
|
1065
|
+
return undefined
|
|
1066
|
+
}
|
|
1067
|
+
const field = varFields[over]?.field
|
|
1068
|
+
if (field !== undefined && "closed" in field) {
|
|
1069
|
+
return field.closed
|
|
1070
|
+
}
|
|
1071
|
+
return undefined
|
|
1072
|
+
}
|
|
1073
|
+
|
|
906
1074
|
/**
|
|
907
1075
|
* Completes one rule: classifies the select record (written order = answer
|
|
908
1076
|
* column order, names must be declaration-order-safe keys), and validates
|
|
@@ -969,7 +1137,15 @@ function completeRule(context: string, state: RuleBuildState, columns: readonly
|
|
|
969
1137
|
}
|
|
970
1138
|
return Object.freeze({
|
|
971
1139
|
items: state.items,
|
|
972
|
-
select: Object.freeze(
|
|
1140
|
+
select: Object.freeze(
|
|
1141
|
+
columns.map(function enrichColumn(column): SelectColumn {
|
|
1142
|
+
return Object.freeze({
|
|
1143
|
+
name: column.name,
|
|
1144
|
+
entry: column.entry,
|
|
1145
|
+
closed: selectClosedOf(state.varFields, column.entry)
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
),
|
|
973
1149
|
varFields: state.varFields,
|
|
974
1150
|
paramUses: state.paramUses
|
|
975
1151
|
})
|
|
@@ -1132,6 +1308,11 @@ interface ProgramState {
|
|
|
1132
1308
|
sealed: boolean
|
|
1133
1309
|
}
|
|
1134
1310
|
|
|
1311
|
+
/** Renders one head column's closed slice for the rule-alignment check's diagnostics. */
|
|
1312
|
+
function renderClosedSlice(closed: ClosedRoster | undefined): string {
|
|
1313
|
+
return closed === undefined ? "a bare value" : `a ${closed.name} reference`
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1135
1316
|
/** Renders one head column's signature for the rule-alignment check. */
|
|
1136
1317
|
function headSignature(column: SelectColumn): string {
|
|
1137
1318
|
const entry = column.entry
|
|
@@ -1148,32 +1329,91 @@ function headSignature(column: SelectColumn): string {
|
|
|
1148
1329
|
return `${column.name}:${agg.op}`
|
|
1149
1330
|
}
|
|
1150
1331
|
|
|
1332
|
+
/** The roster a param anchor carries: present exactly on a closed-reference field anchor. */
|
|
1333
|
+
function anchorRosterOf(anchor: AnyField | "measure" | undefined): ClosedRoster | undefined {
|
|
1334
|
+
if (anchor === undefined || anchor === "measure") {
|
|
1335
|
+
return undefined
|
|
1336
|
+
}
|
|
1337
|
+
if ("closed" in anchor) {
|
|
1338
|
+
return anchor.closed
|
|
1339
|
+
}
|
|
1340
|
+
return undefined
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Renders one param anchor's closedness for the registry's coherence diagnostics. */
|
|
1344
|
+
function renderParamAnchor(roster: ClosedRoster | undefined): string {
|
|
1345
|
+
return roster === undefined ? "a non-closed position" : `a ${roster.name} reference`
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1151
1348
|
/**
|
|
1152
1349
|
* Folds every rule's param uses (recs in declaration order first, output
|
|
1153
1350
|
* rules last — exactly the lowering walk) into the query's registry:
|
|
1154
1351
|
* first use mints the dense `ParamId`, the first FIELD-ANCHORED use types
|
|
1155
|
-
* the wire, and one name must keep one shape
|
|
1352
|
+
* the wire, and one name must keep one shape AND one closedness — every
|
|
1353
|
+
* anchored use of one name must agree on the roster (value identity), so a
|
|
1354
|
+
* param anchored at a closed reference is GUARANTEED to ride the one
|
|
1355
|
+
* roster-verification point (`taggedHandleId`) at execute; a name anchored
|
|
1356
|
+
* both at a closed reference and at a non-closed position (or at two
|
|
1357
|
+
* vocabularies) is refused here, because the wire would translate only the
|
|
1358
|
+
* first anchor's reading (the type tier intersects the uses to `never`;
|
|
1359
|
+
* this is its runtime twin for untyped callers). A param whose anchor is a
|
|
1360
|
+
* CLOSED reference must never sit in an order-comparison position — the
|
|
1361
|
+
* anchor types its value a handle name and the engine would order the
|
|
1362
|
+
* translated row ids, so the pairing is refused here too (the registry is
|
|
1363
|
+
* the one place a name's every use and its anchoring field meet).
|
|
1156
1364
|
*/
|
|
1157
1365
|
function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]): readonly ParamEntry[] {
|
|
1158
1366
|
const order: string[] = []
|
|
1159
|
-
const byName = new Map<
|
|
1367
|
+
const byName = new Map<
|
|
1368
|
+
string,
|
|
1369
|
+
{
|
|
1370
|
+
shape: ParamEntry["shape"]
|
|
1371
|
+
anchor: ParamEntry["anchor"]
|
|
1372
|
+
op: ParamEntry["op"]
|
|
1373
|
+
members: ParamEntry["members"]
|
|
1374
|
+
orderOp: "lt" | "le" | "gt" | "ge" | "pointIn" | undefined
|
|
1375
|
+
}
|
|
1376
|
+
>()
|
|
1160
1377
|
function fold(uses: readonly ParamUse[]): void {
|
|
1161
1378
|
for (const use of uses) {
|
|
1162
1379
|
const existing = byName.get(use.name)
|
|
1163
1380
|
if (existing === undefined) {
|
|
1164
1381
|
order.push(use.name)
|
|
1165
|
-
byName.set(use.name, {
|
|
1382
|
+
byName.set(use.name, {
|
|
1383
|
+
shape: use.shape,
|
|
1384
|
+
anchor: use.anchor,
|
|
1385
|
+
op: use.op,
|
|
1386
|
+
members: use.members,
|
|
1387
|
+
orderOp: isOrderOp(use.op) ? use.op : undefined
|
|
1388
|
+
})
|
|
1166
1389
|
continue
|
|
1167
1390
|
}
|
|
1391
|
+
if ((existing.members === undefined) !== (use.members === undefined)) {
|
|
1392
|
+
throw errors.new(
|
|
1393
|
+
`query param ${use.name} collides with a membership array's registry entry — name the param differently`
|
|
1394
|
+
)
|
|
1395
|
+
}
|
|
1168
1396
|
if (existing.shape !== use.shape) {
|
|
1169
1397
|
throw errors.new(
|
|
1170
1398
|
`query param ${use.name} is used both as a ${existing.shape} param and a ${use.shape} param — one name, one shape`
|
|
1171
1399
|
)
|
|
1172
1400
|
}
|
|
1401
|
+
if (existing.anchor !== undefined && use.anchor !== undefined) {
|
|
1402
|
+
const registered = anchorRosterOf(existing.anchor)
|
|
1403
|
+
const anchored = anchorRosterOf(use.anchor)
|
|
1404
|
+
if (registered !== anchored) {
|
|
1405
|
+
throw errors.new(
|
|
1406
|
+
`query param ${use.name} is anchored at ${renderParamAnchor(registered)} and at ${renderParamAnchor(anchored)} — a closed-anchored param translates handle names through ONE roster (one name, one domain); name the params differently`
|
|
1407
|
+
)
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1173
1410
|
if (existing.anchor === undefined && use.anchor !== undefined) {
|
|
1174
1411
|
existing.anchor = use.anchor
|
|
1175
1412
|
existing.op = use.op
|
|
1176
1413
|
}
|
|
1414
|
+
if (existing.orderOp === undefined && isOrderOp(use.op)) {
|
|
1415
|
+
existing.orderOp = use.op
|
|
1416
|
+
}
|
|
1177
1417
|
}
|
|
1178
1418
|
}
|
|
1179
1419
|
for (const rec of recs) {
|
|
@@ -1190,7 +1430,15 @@ function paramRegistryOf(recs: readonly RecData[], rules: readonly RuleData[]):
|
|
|
1190
1430
|
if (entry === undefined) {
|
|
1191
1431
|
throw errors.new(`query param ${name} lost its registry entry`)
|
|
1192
1432
|
}
|
|
1193
|
-
|
|
1433
|
+
if (
|
|
1434
|
+
entry.orderOp !== undefined &&
|
|
1435
|
+
entry.anchor !== undefined &&
|
|
1436
|
+
entry.anchor !== "measure" &&
|
|
1437
|
+
"closed" in entry.anchor
|
|
1438
|
+
) {
|
|
1439
|
+
throw closedOrderError(`query param ${name}`, `its ${entry.orderOp} use's anchor`, entry.anchor.closed.name)
|
|
1440
|
+
}
|
|
1441
|
+
return Object.freeze({ name, shape: entry.shape, anchor: entry.anchor, op: entry.op, members: entry.members })
|
|
1194
1442
|
})
|
|
1195
1443
|
)
|
|
1196
1444
|
}
|
|
@@ -1221,6 +1469,19 @@ function makeRawQuery(theory: AnySchema, recs: readonly RecData[], rules: readon
|
|
|
1221
1469
|
`every rule of a query derives the same head — rule 0 selects (${signature}), rule ${index} selects (${candidate})`
|
|
1222
1470
|
)
|
|
1223
1471
|
}
|
|
1472
|
+
// The closed slice is part of the head too: one answer column decodes
|
|
1473
|
+
// through one roster, so a union whose rules bind a column at
|
|
1474
|
+
// different vocabularies (or one closed, one bare — the ids would
|
|
1475
|
+
// mistranslate silently) is refused pointed. Vocabulary identity is
|
|
1476
|
+
// value identity, the SDK's membership rule everywhere.
|
|
1477
|
+
rule.select.forEach(function verifyClosedSlice(column, position) {
|
|
1478
|
+
const lead = first.select[position]
|
|
1479
|
+
if (lead !== undefined && column.closed !== lead.closed) {
|
|
1480
|
+
throw errors.new(
|
|
1481
|
+
`every rule of a query derives the same head — the answer column ${lead.name} is ${renderClosedSlice(lead.closed)} in rule 0 but ${renderClosedSlice(column.closed)} in rule ${index} (one column decodes through one roster)`
|
|
1482
|
+
)
|
|
1483
|
+
}
|
|
1484
|
+
})
|
|
1224
1485
|
})
|
|
1225
1486
|
const data: QueryData = Object.freeze({
|
|
1226
1487
|
recs: Object.freeze([...recs]),
|
|
@@ -1305,25 +1566,30 @@ function isIntervalShaped(value: unknown): value is { readonly start: bigint; re
|
|
|
1305
1566
|
}
|
|
1306
1567
|
|
|
1307
1568
|
/**
|
|
1308
|
-
* Tags one closed-reference literal: the
|
|
1309
|
-
*
|
|
1310
|
-
* make any
|
|
1311
|
-
* never handle
|
|
1569
|
+
* Tags one closed-reference literal: the handle NAME, verified against the
|
|
1570
|
+
* roster (the belt the wide fallback type cannot provide — structural
|
|
1571
|
+
* values make any string spellable here) and translated to its
|
|
1572
|
+
* declaration-order row id, tagged u64 — queries cross ids, never handle
|
|
1573
|
+
* names; the wire is untouched. THE single roster-verification point of
|
|
1574
|
+
* the query surface: atom-binding literals, comparison literals,
|
|
1575
|
+
* execute-time params, and membership-array members all reach it (never
|
|
1576
|
+
* duplicate the check per call site).
|
|
1312
1577
|
*/
|
|
1313
1578
|
function taggedHandleId(
|
|
1314
1579
|
context: string,
|
|
1315
1580
|
closed: { readonly name: string; readonly handles: readonly string[] },
|
|
1316
1581
|
value: unknown
|
|
1317
1582
|
): TaggedValue {
|
|
1318
|
-
if (typeof value !== "
|
|
1319
|
-
throw literalShapeError(context, `a ${closed.name} handle
|
|
1583
|
+
if (typeof value !== "string") {
|
|
1584
|
+
throw literalShapeError(context, `a ${closed.name} handle name (string)`, value)
|
|
1320
1585
|
}
|
|
1321
|
-
|
|
1586
|
+
const id = closed.handles.indexOf(value)
|
|
1587
|
+
if (id < 0) {
|
|
1322
1588
|
throw errors.new(
|
|
1323
|
-
`${context}:
|
|
1589
|
+
`${context}: "${value}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
|
|
1324
1590
|
)
|
|
1325
1591
|
}
|
|
1326
|
-
return { kind: "u64", value }
|
|
1592
|
+
return { kind: "u64", value: BigInt(id) }
|
|
1327
1593
|
}
|
|
1328
1594
|
|
|
1329
1595
|
/**
|
|
@@ -1504,7 +1770,12 @@ function lowerAtom(ctx: LowerContext, atom: AtomData, ids: VarIds): AtomIr {
|
|
|
1504
1770
|
return { source: { kind: "edb", relation: relationId }, bindings }
|
|
1505
1771
|
}
|
|
1506
1772
|
|
|
1507
|
-
/**
|
|
1773
|
+
/**
|
|
1774
|
+
* Lowers one binding term. A membership ARRAY (`literalSet`) lowers to the
|
|
1775
|
+
* existing param-set term over its content-addressed registry entry — the
|
|
1776
|
+
* program IR is byte-identical to the same set spelled `r.inSet`; the SDK
|
|
1777
|
+
* supplies the translated member set itself at execute (`wireParams`).
|
|
1778
|
+
*/
|
|
1508
1779
|
function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEntry, ids: VarIds): TermIr {
|
|
1509
1780
|
const bound = binding.term
|
|
1510
1781
|
switch (bound.kind) {
|
|
@@ -1514,6 +1785,8 @@ function lowerBindingTerm(ctx: LowerContext, context: string, binding: BindingEn
|
|
|
1514
1785
|
return { kind: "param", param: paramIdOf(ctx, bound.name) }
|
|
1515
1786
|
case "setParam":
|
|
1516
1787
|
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
|
|
1788
|
+
case "literalSet":
|
|
1789
|
+
return { kind: "paramSet", param: paramIdOf(ctx, bound.name) }
|
|
1517
1790
|
case "literal":
|
|
1518
1791
|
return { kind: "literal", value: taggedLiteral(context, binding.data, bound.value) }
|
|
1519
1792
|
}
|
package/src/query/run.ts
CHANGED
|
@@ -8,12 +8,16 @@
|
|
|
8
8
|
* values — the marshal boundary is pure both ways: the engine computed
|
|
9
9
|
* the answer under the prepared head, so a decoded row that carries every
|
|
10
10
|
* select column IS a row (the trusted read seam), and nothing is asserted
|
|
11
|
-
* on any value.
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* on any value. A CLOSED answer column decodes id → handle NAME through
|
|
12
|
+
* the marshal's one bijection (`handleOf` — the same read half every fact
|
|
13
|
+
* decode rides; the column's roster rides `SelectColumn.closed`), so query
|
|
14
|
+
* rows speak the vocabulary exactly as scans and gets do. Answers are
|
|
15
|
+
* SETS — no order or limit exists anywhere; hosts sort. The `Prepared`
|
|
16
|
+
* VALUE itself (no lifecycle, GC-reclaimed plan) lives in `#db.ts`.
|
|
14
17
|
*/
|
|
15
18
|
|
|
16
19
|
import * as errors from "@superbuilders/errors"
|
|
20
|
+
import { handleOf } from "#marshal.ts"
|
|
17
21
|
import type { FactValue, QueryParam, TaggedValue } from "#native.ts"
|
|
18
22
|
import type { SelectColumn } from "#query/atom.ts"
|
|
19
23
|
import { taggedCmpLiteral } from "#query/lower.ts"
|
|
@@ -45,10 +49,23 @@ function wireValue(entry: ParamEntry, context: string, value: unknown): TaggedVa
|
|
|
45
49
|
* in registry order (= the lowering's dense `ParamId`s). A missing entry
|
|
46
50
|
* is a typed error naming the param; values tag by the anchoring use's
|
|
47
51
|
* structural type; a set param takes a readonly array (the empty set is
|
|
48
|
-
* legal and matches nothing — the engine's rule).
|
|
52
|
+
* legal and matches nothing — the engine's rule). A MEMBERSHIP-ARRAY
|
|
53
|
+
* entry (`members` present — a literal set folded into the program) is
|
|
54
|
+
* supplied by the SDK itself: each handle name rides the one
|
|
55
|
+
* roster-verification point (`taggedHandleId`, through `wireValue`) and
|
|
56
|
+
* crosses as the same `{ kind: "set", values }` a bound `r.inSet` param
|
|
57
|
+
* crosses as — the host's params object is never consulted for it.
|
|
49
58
|
*/
|
|
50
59
|
function wireParams(entries: readonly ParamEntry[], supplied: Readonly<Record<string, unknown>>): QueryParam[] {
|
|
51
60
|
return entries.map(function wireOne(entry): QueryParam {
|
|
61
|
+
if (entry.members !== undefined) {
|
|
62
|
+
return {
|
|
63
|
+
kind: "set",
|
|
64
|
+
values: entry.members.map(function wireMember(member, index) {
|
|
65
|
+
return wireValue(entry, `membership array ${entry.name}[${index}]`, member)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
52
69
|
const value = supplied[entry.name]
|
|
53
70
|
if (value === undefined) {
|
|
54
71
|
throw errors.new(`execute params object is missing param ${entry.name}`)
|
|
@@ -90,7 +107,9 @@ function isAnswerRow<Row>(
|
|
|
90
107
|
/**
|
|
91
108
|
* Decodes positional answer rows (column order = the program's head order
|
|
92
109
|
* = the select's written order) to named, frozen row objects of bare
|
|
93
|
-
* structural values.
|
|
110
|
+
* structural values. A closed column lifts its row id back to the handle
|
|
111
|
+
* NAME through the marshal's bijection — an out-of-roster id is the same
|
|
112
|
+
* pointed throw a fact decode gives, never a silent fallback.
|
|
94
113
|
*/
|
|
95
114
|
function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]): Row[] {
|
|
96
115
|
return rows.map(function decodeRow(row) {
|
|
@@ -103,7 +122,8 @@ function decodeAnswers<Row>(select: readonly SelectColumn[], rows: FactValue[][]
|
|
|
103
122
|
if (cell === undefined) {
|
|
104
123
|
throw errors.new(`query answer cell ${ordinal} (${column.name}) is absent`)
|
|
105
124
|
}
|
|
106
|
-
decoded[column.name] =
|
|
125
|
+
decoded[column.name] =
|
|
126
|
+
column.closed === undefined ? cell : handleOf(`query answer column ${column.name}`, column.closed, cell)
|
|
107
127
|
})
|
|
108
128
|
Object.freeze(decoded)
|
|
109
129
|
if (!isAnswerRow<Row>(select, decoded)) {
|