@bjornpagen/bumbledb 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/COOKBOOK.md +176 -36
  2. package/README.md +70 -26
  3. package/dist/capacity.d.ts +15 -55
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +63 -12
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +28 -6
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +119 -71
  10. package/dist/changes.js.map +1 -1
  11. package/dist/closed.d.ts +23 -51
  12. package/dist/closed.d.ts.map +1 -1
  13. package/dist/closed.js +110 -168
  14. package/dist/closed.js.map +1 -1
  15. package/dist/codec.d.ts +8 -14
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +113 -188
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +17 -18
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +41 -20
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +76 -41
  24. package/dist/db-native.d.ts.map +1 -1
  25. package/dist/db-native.js +8 -0
  26. package/dist/db-native.js.map +1 -1
  27. package/dist/db.d.ts +61 -26
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +152 -85
  30. package/dist/db.js.map +1 -1
  31. package/dist/errors.d.ts +11 -3
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +13 -1
  34. package/dist/errors.js.map +1 -1
  35. package/dist/face.d.ts +19 -25
  36. package/dist/face.d.ts.map +1 -1
  37. package/dist/face.js +30 -24
  38. package/dist/face.js.map +1 -1
  39. package/dist/fields.d.ts +13 -26
  40. package/dist/fields.d.ts.map +1 -1
  41. package/dist/fields.js +89 -116
  42. package/dist/fields.js.map +1 -1
  43. package/dist/immutable.d.ts +10 -0
  44. package/dist/immutable.d.ts.map +1 -0
  45. package/dist/immutable.js +75 -0
  46. package/dist/immutable.js.map +1 -0
  47. package/dist/index.d.ts +16 -12
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +6 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +6 -4
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +3 -1
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +11 -11
  56. package/dist/law.d.ts.map +1 -1
  57. package/dist/law.js +1 -1
  58. package/dist/law.js.map +1 -1
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +11 -6
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +2 -3
  63. package/dist/migration.d.ts.map +1 -1
  64. package/dist/migration.js +5 -5
  65. package/dist/migration.js.map +1 -1
  66. package/dist/native.d.ts +2 -2
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js.map +1 -1
  69. package/dist/query/atom.d.ts +1 -1
  70. package/dist/query/atom.d.ts.map +1 -1
  71. package/dist/query/atom.js.map +1 -1
  72. package/dist/query/description.d.ts +33 -0
  73. package/dist/query/description.d.ts.map +1 -0
  74. package/dist/query/description.js +302 -0
  75. package/dist/query/description.js.map +1 -0
  76. package/dist/query/lower.d.ts +60 -4
  77. package/dist/query/lower.d.ts.map +1 -1
  78. package/dist/query/lower.js +126 -161
  79. package/dist/query/lower.js.map +1 -1
  80. package/dist/query/parse-ir.d.ts +7 -2
  81. package/dist/query/parse-ir.d.ts.map +1 -1
  82. package/dist/query/parse-ir.js +240 -53
  83. package/dist/query/parse-ir.js.map +1 -1
  84. package/dist/query/run.d.ts.map +1 -1
  85. package/dist/query/run.js +8 -10
  86. package/dist/query/run.js.map +1 -1
  87. package/dist/query/scope.d.ts +3 -4
  88. package/dist/query/scope.d.ts.map +1 -1
  89. package/dist/query/scope.js +5 -10
  90. package/dist/query/scope.js.map +1 -1
  91. package/dist/relation.d.ts +22 -48
  92. package/dist/relation.d.ts.map +1 -1
  93. package/dist/relation.js +30 -82
  94. package/dist/relation.js.map +1 -1
  95. package/dist/result.d.ts +10 -25
  96. package/dist/result.d.ts.map +1 -1
  97. package/dist/result.js +7 -12
  98. package/dist/result.js.map +1 -1
  99. package/dist/rows.d.ts +11 -14
  100. package/dist/rows.d.ts.map +1 -1
  101. package/dist/rows.js +49 -237
  102. package/dist/rows.js.map +1 -1
  103. package/dist/runtime-codes.d.ts +1 -1
  104. package/dist/runtime-codes.d.ts.map +1 -1
  105. package/dist/runtime-codes.js +1 -2
  106. package/dist/runtime-codes.js.map +1 -1
  107. package/dist/runtime-errors.d.ts +27 -15
  108. package/dist/runtime-errors.d.ts.map +1 -1
  109. package/dist/runtime-errors.js +44 -8
  110. package/dist/runtime-errors.js.map +1 -1
  111. package/dist/runtime-native.d.ts +16 -34
  112. package/dist/runtime-native.d.ts.map +1 -1
  113. package/dist/runtime-native.js.map +1 -1
  114. package/dist/runtime.d.ts +11 -28
  115. package/dist/runtime.d.ts.map +1 -1
  116. package/dist/runtime.js +14 -54
  117. package/dist/runtime.js.map +1 -1
  118. package/dist/schema.d.ts +9 -1
  119. package/dist/schema.d.ts.map +1 -1
  120. package/dist/schema.js +85 -24
  121. package/dist/schema.js.map +1 -1
  122. package/dist/selection.d.ts +27 -0
  123. package/dist/selection.d.ts.map +1 -0
  124. package/dist/selection.js +104 -0
  125. package/dist/selection.js.map +1 -0
  126. package/dist/shape.d.ts +6 -9
  127. package/dist/shape.d.ts.map +1 -1
  128. package/dist/statements.d.ts +17 -52
  129. package/dist/statements.d.ts.map +1 -1
  130. package/dist/statements.js +92 -111
  131. package/dist/statements.js.map +1 -1
  132. package/dist/values.d.ts +25 -0
  133. package/dist/values.d.ts.map +1 -0
  134. package/dist/values.js +203 -0
  135. package/dist/values.js.map +1 -0
  136. package/pack-provenance.json +3 -3
  137. package/package.json +4 -4
  138. package/src/capacity.ts +86 -76
  139. package/src/changes.ts +225 -94
  140. package/src/closed.ts +161 -314
  141. package/src/codec.ts +123 -213
  142. package/src/compile.ts +61 -38
  143. package/src/db-native.ts +73 -68
  144. package/src/db.ts +268 -165
  145. package/src/errors.ts +16 -1
  146. package/src/face.ts +44 -74
  147. package/src/fields.ts +100 -146
  148. package/src/immutable.ts +76 -0
  149. package/src/index.ts +38 -31
  150. package/src/internal/log.ts +4 -5
  151. package/src/law.ts +14 -14
  152. package/src/lower.ts +13 -10
  153. package/src/migration.ts +5 -12
  154. package/src/native.ts +2 -1
  155. package/src/query/atom.ts +1 -0
  156. package/src/query/description.ts +394 -0
  157. package/src/query/lower.ts +139 -173
  158. package/src/query/parse-ir.ts +247 -54
  159. package/src/query/run.ts +15 -9
  160. package/src/query/scope.ts +10 -18
  161. package/src/relation.ts +58 -142
  162. package/src/result.ts +16 -45
  163. package/src/rows.ts +57 -250
  164. package/src/runtime-codes.ts +1 -2
  165. package/src/runtime-errors.ts +48 -8
  166. package/src/runtime-native.ts +13 -46
  167. package/src/runtime.ts +51 -119
  168. package/src/schema.ts +99 -29
  169. package/src/selection.ts +124 -0
  170. package/src/shape.ts +9 -9
  171. package/src/statements.ts +138 -189
  172. package/src/values.ts +223 -0
package/src/result.ts CHANGED
@@ -5,31 +5,22 @@ import { dbNative } from "#db-native.ts"
5
5
  import type { FindColumn } from "#query/atom.ts"
6
6
  import { decodeAnswers } from "#query/run.ts"
7
7
  import type { CellValue } from "#rows.ts"
8
- import type { ExecutionPolicy } from "#runtime.ts"
9
- import { deliveryResultBytes, nativeOperationWith, policyWire } from "#runtime.ts"
8
+ import { nativeOperationWith } from "#runtime.ts"
10
9
  import type { CloseReport, DbError } from "#runtime-errors.ts"
11
10
 
12
11
  /**
13
- * `CompleteResult<A>` the sealed owner of one completed query answer:
14
- * published only after all evaluation/finalization succeeded,
15
- * possibly backed by temporary LMDB scratch. Owned and independent of its
16
- * source snapshot/session.
12
+ * One complete answer set, sealed after evaluation and independent of its
13
+ * source snapshot. collect() explicitly materializes all rows into JS and
14
+ * leaves the result available. pages() consumes it once through bounded
15
+ * delivery batches; it does not stream query execution.
17
16
  *
18
- * `collect` materializes a bounded owned array and leaves the result
19
- * available; it refuses (`ResourceLimit`) before allocating past
20
- * `maxBytes`, and a cap failure leaves the sealed backing available for
21
- * `pages`. `pages` is a one-shot consuming stream over the
22
- * completed result: its first execution atomically spends the result and
23
- * moves the backing storage into a private cursor owned by the stream's
24
- * scope — construction alone spends nothing, a second run fails
25
- * `SpentHandle`, and a run after the result's scope closed fails
26
- * `ClosedHandle`. Early take, downstream failure and interruption all
27
- * close/drain the private cursor; EOF cleanup is identical. There is no
28
- * public cursor, `next`, AsyncIterable, clone or second streaming API.
17
+ * The stream owns its cursor through Effect scope. Early termination,
18
+ * failure, interruption and EOF close/drain it. Creating the stream does
19
+ * not spend the result; running it twice fails with SpentHandle.
29
20
  */
30
21
  interface CompleteResult<A> {
31
- collect(options: { readonly maxBytes: bigint }, work: ExecutionPolicy): Effect.Effect<ReadonlyArray<A>, DbError>
32
- pages(options: { readonly pageBytes: bigint }, work: ExecutionPolicy): Stream.Stream<ReadonlyArray<A>, DbError>
22
+ collect(): Effect.Effect<ReadonlyArray<A>, DbError>
23
+ pages(): Stream.Stream<ReadonlyArray<A>, DbError>
33
24
  close(): Effect.Effect<CloseReport>
34
25
  }
35
26
 
@@ -52,42 +43,27 @@ function decodePage<A>(finds: readonly FindColumn[], rows: readonly (readonly Ce
52
43
 
53
44
  /**
54
45
  * Internal constructor: `db.ts` publishes results through this after
55
- * execution completes. Delivery (`collect`/`pages`) starts a fresh bounded
56
- * operation under the caller's delivery policy — never the completed query's
57
- * expired execution deadline.
46
+ * execution completes. Each delivery has independent cooperative cancellation.
58
47
  */
59
48
  function makeCompleteResult<A>(handle: ResultHandle, finds: readonly FindColumn[]): CompleteResult<A> {
60
- function deliveryWire(operation: string, delivery: ExecutionPolicy, requested: bigint) {
61
- return policyWire({ ...delivery, resultBytes: deliveryResultBytes(requested, delivery) }, operation)
62
- }
63
49
  const value: CompleteResult<A> = {
64
- collect(options, work) {
50
+ collect() {
65
51
  return Effect.suspend(() =>
66
52
  nativeOperationWith(
67
53
  "CompleteResult.collect",
68
- (callback) =>
69
- dbNative.runtimeResultCollect(
70
- handle,
71
- deliveryWire("CompleteResult.collect", work, options.maxBytes),
72
- callback
73
- ),
54
+ (callback) => dbNative.runtimeResultCollect(handle, callback),
74
55
  dbNative.runtimeRowsTake,
75
56
  (rows) => decodePage<A>(finds, rows)
76
57
  )
77
58
  )
78
59
  },
79
- pages(options, work) {
60
+ pages() {
80
61
  return Stream.unwrap(
81
62
  Effect.gen(function* () {
82
63
  const cursor: CursorHandle = yield* Effect.acquireRelease(
83
64
  nativeOperationWith(
84
65
  "CompleteResult.pages",
85
- (callback) =>
86
- dbNative.runtimeResultCursor(
87
- handle,
88
- deliveryWire("CompleteResult.pages", work, options.pageBytes),
89
- callback
90
- ),
66
+ (callback) => dbNative.runtimeResultCursor(handle, callback),
91
67
  dbNative.runtimeCursorTake,
92
68
  (taken) => taken
93
69
  ),
@@ -97,12 +73,7 @@ function makeCompleteResult<A>(handle: ResultHandle, finds: readonly FindColumn[
97
73
  return Stream.paginate(undefined, () =>
98
74
  nativeOperationWith(
99
75
  "CompleteResult.page",
100
- (callback) =>
101
- dbNative.runtimeCursorNext(
102
- cursor,
103
- deliveryWire("CompleteResult.page", work, options.pageBytes),
104
- callback
105
- ),
76
+ (callback) => dbNative.runtimeCursorNext(cursor, callback),
106
77
  dbNative.runtimePageTake,
107
78
  (page) => page
108
79
  ).pipe(
package/src/rows.ts CHANGED
@@ -17,12 +17,9 @@ import { AuthoringError, SdkInvariantError } from "#errors.ts"
17
17
  * scans and byte copies.
18
18
  */
19
19
  import type { AnyClosedRoster, AnyField } from "#fields.ts"
20
- import { isFloatIntervalValue, isIntervalValue, literalShapeError, rosterOf } from "#fields.ts"
21
- import type { AnyRelation, Fact, RelationData } from "#relation.ts"
22
- import { Uuid } from "#uuid.ts"
23
-
24
- /** Converter-granularity host cell wall (64 KiB). One oversize cell refuses. */
25
- const HOST_CELL_MAX = 65536n
20
+ import { literalShapeError, rosterOf } from "#fields.ts"
21
+ import { type AnyRelation, type Fact, relationFields } from "#relation.ts"
22
+ import { fieldValue, recordValue } from "#values.ts"
26
23
 
27
24
  /**
28
25
  * One owned cell at the private bridge boundary. The declared sealed field
@@ -44,29 +41,18 @@ type CellValue =
44
41
  interface FlatRows {
45
42
  readonly rows: bigint
46
43
  readonly cells: readonly CellValue[]
47
- /** Conservative charged byte size of the owned cells (input accounting). */
48
- readonly bytes: bigint
49
44
  }
50
45
 
51
46
  function recordOf(fact: object): Readonly<Record<string, unknown>> {
52
- if (typeof fact !== "object" && typeof fact !== "function") {
53
- throw new AuthoringError({ message: "fact object is not string-indexable" })
47
+ if (typeof fact !== "object" || fact === null) {
48
+ throw new AuthoringError({ message: "expected a fact record" })
54
49
  }
55
50
  return fact as Readonly<Record<string, unknown>>
56
51
  }
57
52
 
58
- function refuseShared(context: string, value: Uint8Array): void {
59
- if (!(value.buffer instanceof ArrayBuffer)) {
60
- throw new AuthoringError({
61
- message: `${context}: SharedArrayBuffer-backed views are refused — make a synchronized stable copy into ordinary unshared input first`
62
- })
63
- }
64
- }
65
-
66
53
  /**
67
- * Cheap host charge before any string scan or byte copy. Strings charge
68
- * two bytes per UTF-16 code unit, not their UTF-8 wire size. Native admission
69
- * measures and bounds UTF-8 separately; byte views charge their length.
54
+ * Approximate payload size for host batching, not a memory allowance.
55
+ * Strings use two bytes per UTF-16 code unit, not their UTF-8 wire size.
70
56
  */
71
57
  function hostCellCharge(value: unknown): bigint {
72
58
  if (typeof value === "string") {
@@ -87,36 +73,11 @@ function hostCellCharge(value: unknown): bigint {
87
73
  return 0n
88
74
  }
89
75
 
90
- function assertHostCellFits(context: string, value: unknown, limit: bigint): bigint {
91
- const charge = hostCellCharge(value)
92
- if (charge > limit) {
93
- throw new AuthoringError({
94
- message: `${context}: host cell of ${charge} bytes exceeds the ${limit}-byte converter bound — refuse before copy or scan`
95
- })
96
- }
97
- return charge
98
- }
99
-
100
- /**
101
- * The write half of the closed bijection: one handle NAME to its u64 row
102
- * id (declaration order = row ids). An unknown name is a pointed refusal
103
- * naming the vocabulary and its roster.
104
- */
105
- function closedCellOf(context: string, closed: AnyClosedRoster, name: string): CellValue {
106
- const id = closed.handles.indexOf(name)
107
- if (id === -1) {
108
- throw new AuthoringError({
109
- message: `${context}: "${name}" is not a handle of ${closed.name} — the roster is ${closed.handles.join(", ")}`
110
- })
111
- }
112
- return BigInt(id)
113
- }
114
-
115
76
  function handleOf(context: string, closed: AnyClosedRoster, cell: unknown): string {
116
77
  if (typeof cell !== "bigint") {
117
78
  throw literalShapeError(context, `a ${closed.name} handle id (bigint)`, cell)
118
79
  }
119
- const handle = closed.handles[Number(cell)]
80
+ const handle = cell < 0n || cell >= BigInt(closed.handles.length) ? undefined : closed.handles[Number(cell)]
120
81
  if (handle === undefined) {
121
82
  throw new AuthoringError({
122
83
  message: `${context}: id ${cell} is outside the ${closed.name} roster (${closed.handles.join(", ")})`
@@ -125,227 +86,63 @@ function handleOf(context: string, closed: AnyClosedRoster, cell: unknown): stri
125
86
  return handle
126
87
  }
127
88
 
128
- /** Conservative owned-byte charge of one cell (host accounting, not wire). */
129
- function cellBytes(cell: CellValue): bigint {
130
- if (typeof cell === "boolean") {
131
- return 1n
132
- }
133
- if (typeof cell === "bigint" || typeof cell === "number") {
134
- return 8n
135
- }
136
- if (typeof cell === "string") {
137
- // Account for the host string here; native admission separately
138
- // checks and charges its exact UTF-8 encoding.
139
- return BigInt(cell.length) * 2n
140
- }
141
- if (cell instanceof Uint8Array) {
142
- return BigInt(cell.byteLength)
89
+ function cellOf(context: string, field: AnyField, value: unknown): CellValue {
90
+ if ("closed" in field) {
91
+ return BigInt(field.closed.handles.indexOf(fieldValue(context, field, value)))
143
92
  }
144
- return 16n
93
+ return fieldValue(context, field, value)
145
94
  }
146
95
 
147
- function cellOf(context: string, field: AnyField, value: unknown): CellValue {
148
- const roster = rosterOf(field)
149
- if (roster !== undefined) {
150
- if (typeof value !== "string") {
151
- throw literalShapeError(context, `a ${roster.name} handle name (string)`, value)
152
- }
153
- return closedCellOf(context, roster, value)
154
- }
155
- switch (field.kind) {
156
- case "bool": {
157
- if (typeof value !== "boolean") {
158
- throw literalShapeError(context, "boolean", value)
159
- }
160
- return value
161
- }
162
- case "u64":
163
- case "i64": {
164
- if (typeof value !== "bigint") {
165
- throw literalShapeError(context, "bigint", value)
166
- }
167
- return value
168
- }
169
- case "str": {
170
- if (typeof value !== "string") {
171
- throw literalShapeError(context, "string", value)
172
- }
173
- assertHostCellFits(context, value, HOST_CELL_MAX)
174
- if (!value.isWellFormed()) {
175
- throw literalShapeError(context, "well-formed string", value)
176
- }
177
- return value
178
- }
179
- case "f64": {
180
- if (typeof value !== "number") {
181
- throw literalShapeError(context, "number", value)
182
- }
183
- return value
184
- }
185
- case "uuid": {
186
- if (!Uuid.isUuid(value)) {
187
- throw literalShapeError(context, "a UUID (canonical UUID text)", value)
188
- }
189
- // UUIDs cross as canonical hyphenated text, not raw bytes.
190
- return value
191
- }
192
- case "bytes": {
193
- if (!(value instanceof Uint8Array)) {
194
- throw literalShapeError(context, "Uint8Array", value)
195
- }
196
- refuseShared(context, value)
197
- assertHostCellFits(context, value, HOST_CELL_MAX)
198
- if (value.byteLength !== field.width) {
199
- throw new AuthoringError({
200
- message: `${context}: bytes<${field.width}> takes exactly ${field.width} bytes (got ${value.byteLength})`
201
- })
202
- }
203
- // Owned copy at the ownership boundary: later caller mutation
204
- // cannot change an accepted cell.
205
- return Uint8Array.from(value)
206
- }
207
- case "interval": {
208
- if (field.element === "f64") {
209
- if (!isFloatIntervalValue(value)) {
210
- throw literalShapeError(context, "float interval ({ start, end } numbers)", value)
211
- }
212
- if (Number.isNaN(value.start) || Number.isNaN(value.end)) {
213
- throw new AuthoringError({ message: `${context}: a float interval endpoint cannot be NaN` })
214
- }
215
- const start = Object.is(value.start, -0) ? 0 : value.start
216
- const end = Object.is(value.end, -0) ? 0 : value.end
217
- if (!(start < end)) {
218
- throw new AuthoringError({
219
- message: `${context}: a float interval is half-open and nonempty (start < end strictly)`
220
- })
221
- }
222
- return { start, end }
223
- }
224
- if (!isIntervalValue(value)) {
225
- throw literalShapeError(context, "interval ({ start, end } bigints)", value)
226
- }
227
- return { start: value.start, end: value.end }
228
- }
229
- }
96
+ function factCellsOf(data: AnyRelation, fact: unknown): CellValue[] {
97
+ const record = recordValue(
98
+ `relation ${data.name}`,
99
+ fact,
100
+ relationFields(data).map((declared) => declared.name)
101
+ )
102
+ return relationFields(data).map((declared) =>
103
+ cellOf(`relation ${data.name} field ${declared.name}`, declared.field, record[declared.name])
104
+ )
230
105
  }
231
106
 
232
107
  /**
233
108
  * The flat projector: every fact's cells land in ONE row-major cell array
234
- * (length rows × arity) with the row count and charged byte size counted
235
- * while projecting. Missing-field refusal and per-cell judgment are
109
+ * (length rows × arity), counting rows while projecting.
110
+ * Missing-field refusal and per-cell judgment are
236
111
  * {@link cellOf}'s, byte for byte.
237
112
  */
238
- function flatRowsOf(data: RelationData, facts: Iterable<object>): FlatRows {
113
+ function flatRowsOf(data: AnyRelation, facts: Iterable<object>): FlatRows {
239
114
  const cells: CellValue[] = []
240
115
  let rows = 0n
241
- let bytes = 0n
242
116
  for (const fact of facts) {
117
+ for (const cell of factCellsOf(data, fact)) cells.push(cell)
243
118
  rows += 1n
244
- const record = recordOf(fact)
245
- for (const declared of data.fields) {
246
- const value = record[declared.name]
247
- if (value === undefined) {
248
- throw new AuthoringError({ message: `relation ${data.name}: fact is missing field ${declared.name}` })
249
- }
250
- const cell = cellOf(`relation ${data.name} field ${declared.name}`, declared.field, value)
251
- bytes += cellBytes(cell)
252
- cells.push(cell)
253
- }
254
119
  }
255
- return { rows, cells, bytes }
120
+ return { rows, cells }
256
121
  }
257
122
 
258
123
  function keyCellsOf(
259
- data: RelationData,
124
+ data: AnyRelation,
260
125
  projection: readonly string[],
261
- key: Readonly<Record<string, unknown>>
126
+ input: Readonly<Record<string, unknown>>
262
127
  ): CellValue[] {
263
- for (const supplied of Object.keys(key)) {
264
- if (key[supplied] !== undefined && !projection.includes(supplied)) {
265
- throw new AuthoringError({
266
- message: `relation ${data.name}: key object carries field ${supplied} outside the primary key projection (${projection.join(", ")})`
267
- })
268
- }
269
- }
128
+ const key = recordValue(`relation ${data.name} key`, input, projection)
270
129
  return projection.map(function marshalKeyCell(fieldName) {
271
- const declared = data.fields.find(function byName(candidate) {
272
- return candidate.name === fieldName
273
- })
130
+ const declared = relationFields(data).find((candidate) => candidate.name === fieldName)
274
131
  if (declared === undefined) {
275
- throw new AuthoringError({
276
- message: `relation ${data.name}: key projection cites unknown field ${fieldName}`
277
- })
132
+ throw new AuthoringError({ message: `relation ${data.name}: key projection cites unknown field ${fieldName}` })
278
133
  }
279
- const value = key[fieldName]
280
- if (value === undefined) {
281
- throw new AuthoringError({
282
- message: `relation ${data.name}: key object is missing field ${fieldName} — get reads through the primary (first-declared) key, whose projection is (${projection.join(", ")})`
283
- })
284
- }
285
- return cellOf(`relation ${data.name} key field ${fieldName}`, declared.field, value)
134
+ return cellOf(`relation ${data.name} key field ${fieldName}`, declared.field, key[fieldName])
286
135
  })
287
136
  }
288
137
 
289
138
  function decodeCell(context: string, field: AnyField, cell: unknown): unknown {
290
139
  const roster = rosterOf(field)
291
- if (roster !== undefined) {
292
- return handleOf(context, roster, cell)
293
- }
294
- switch (field.kind) {
295
- case "bool": {
296
- if (typeof cell !== "boolean") {
297
- throw new SdkInvariantError({ message: `${context}: expected boolean cell` })
298
- }
299
- return cell
300
- }
301
- case "u64":
302
- case "i64": {
303
- if (typeof cell !== "bigint") {
304
- throw new SdkInvariantError({ message: `${context}: expected bigint cell` })
305
- }
306
- return cell
307
- }
308
- case "f64": {
309
- if (typeof cell !== "number") {
310
- throw new SdkInvariantError({ message: `${context}: expected number cell` })
311
- }
312
- return cell
313
- }
314
- case "str": {
315
- if (typeof cell !== "string") {
316
- throw new SdkInvariantError({ message: `${context}: expected string cell` })
317
- }
318
- return cell
319
- }
320
- case "uuid": {
321
- // The native renderer spells uuid as the canonical
322
- // canonical hyphenated UUID string (marshal.rs `uuid_hex`); the read
323
- // side revalidates the spelling and keeps the string value.
324
- if (!Uuid.isUuid(cell)) {
325
- throw new SdkInvariantError({
326
- message: `${context}: expected an uuid cell (canonical UUID text)`
327
- })
328
- }
329
- return cell
330
- }
331
- case "bytes": {
332
- if (!(cell instanceof Uint8Array)) {
333
- throw new SdkInvariantError({ message: `${context}: expected owned bytes` })
334
- }
335
- return cell
336
- }
337
- case "interval": {
338
- if (field.element === "f64") {
339
- if (!isFloatIntervalValue(cell)) {
340
- throw new SdkInvariantError({ message: `${context}: expected a float interval cell` })
341
- }
342
- return Object.freeze({ start: cell.start, end: cell.end })
343
- }
344
- if (!isIntervalValue(cell)) {
345
- throw new SdkInvariantError({ message: `${context}: expected an integer interval cell` })
346
- }
347
- return Object.freeze({ start: cell.start, end: cell.end })
348
- }
140
+ if (roster !== undefined) return handleOf(context, roster, cell)
141
+ try {
142
+ return fieldValue(context, field, cell)
143
+ } catch (cause) {
144
+ if (cause instanceof AuthoringError) throw new SdkInvariantError({ message: cause.message })
145
+ throw cause
349
146
  }
350
147
  }
351
148
 
@@ -353,7 +150,7 @@ function isCompleteFact<R extends AnyRelation>(
353
150
  relation: R,
354
151
  decoded: Readonly<Record<string, unknown>>
355
152
  ): decoded is Readonly<Record<string, unknown>> & Fact<R> {
356
- return relation.data.fields.every(function present(declared) {
153
+ return relationFields(relation).every(function present(declared) {
357
154
  return decoded[declared.name] !== undefined
358
155
  })
359
156
  }
@@ -363,21 +160,22 @@ function isCompleteFact<R extends AnyRelation>(
363
160
  * declared field order: stable field names and shapes, no Proxy or per-cell closures.
364
161
  */
365
162
  function factOfCells<R extends AnyRelation>(relation: R, row: readonly unknown[]): Fact<R> {
366
- const data = relation.data
367
- if (row.length !== data.fields.length) {
163
+ const data = relation
164
+ if (row.length !== relationFields(data).length) {
368
165
  throw new SdkInvariantError({
369
- message: `relation ${data.name}: row arity ${row.length} does not match the ${data.fields.length} declared fields`
166
+ message: `relation ${data.name}: row arity ${row.length} does not match the ${relationFields(data).length} declared fields`
370
167
  })
371
168
  }
372
169
  const decoded: Record<string, unknown> = {}
373
- data.fields.forEach(function decodeOne(declared, ordinal) {
170
+ relationFields(data).forEach(function decodeOne(declared, ordinal) {
374
171
  const cell = row[ordinal]
375
172
  if (cell === undefined) {
376
173
  throw new SdkInvariantError({
377
174
  message: `relation ${data.name}: row cell ${ordinal} (${declared.name}) is absent`
378
175
  })
379
176
  }
380
- decoded[declared.name] = decodeCell(`relation ${data.name} field ${declared.name}`, declared.field, cell)
177
+ const value = decodeCell(`relation ${data.name} field ${declared.name}`, declared.field, cell)
178
+ setOwnField(decoded, declared.name, value)
381
179
  })
382
180
  Object.freeze(decoded)
383
181
  if (!isCompleteFact(relation, decoded)) {
@@ -386,16 +184,25 @@ function factOfCells<R extends AnyRelation>(relation: R, row: readonly unknown[]
386
184
  return decoded
387
185
  }
388
186
 
187
+ /** Preserve every declared name as data, including JavaScript's prototype setter. */
188
+ function setOwnField(record: Record<string, unknown>, name: string, value: unknown): void {
189
+ if (name === "__proto__") {
190
+ Object.defineProperty(record, name, { value, enumerable: true, writable: true, configurable: true })
191
+ } else {
192
+ record[name] = value
193
+ }
194
+ }
195
+
389
196
  export type { CellValue, FlatRows }
390
197
  export {
391
- assertHostCellFits,
392
- cellBytes,
393
198
  cellOf,
394
199
  decodeCell,
200
+ factCellsOf,
395
201
  factOfCells,
396
202
  flatRowsOf,
397
203
  handleOf,
398
204
  hostCellCharge,
399
205
  keyCellsOf,
400
- recordOf
206
+ recordOf,
207
+ setOwnField
401
208
  }
@@ -13,6 +13,5 @@ export const runtimeErrorCodes = [
13
13
  "Io",
14
14
  "ResourceLimit",
15
15
  "Engine",
16
- "Cancelled",
17
- "DeadlineExceeded"
16
+ "Cancelled"
18
17
  ] as const
@@ -1,4 +1,5 @@
1
1
  import { Schema } from "effect"
2
+ import { AuthoringDiagnostic, AuthoringError } from "#errors.ts"
2
3
  import { runtimeErrorCodes } from "#runtime-codes.ts"
3
4
 
4
5
  export { runtimeErrorCodes } from "#runtime-codes.ts"
@@ -12,7 +13,18 @@ const ResourceLimit = Schema.Struct({
12
13
  })
13
14
  const PlainReason = Schema.Struct({
14
15
  _tag: Schema.Literals(
15
- runtimeErrorCodes.filter((code) => code !== "ResourceLimit" && code !== "Io" && code !== "Engine")
16
+ runtimeErrorCodes.filter(
17
+ (code) => code !== "ResourceLimit" && code !== "Io" && code !== "Engine" && code !== "InvalidArgument"
18
+ )
19
+ )
20
+ })
21
+ const InvalidArgument = Schema.Struct({
22
+ _tag: Schema.Literal("InvalidArgument"),
23
+ detail: Schema.optional(
24
+ Schema.Struct({
25
+ message: Schema.String,
26
+ diagnostic: Schema.optional(AuthoringDiagnostic)
27
+ })
16
28
  )
17
29
  })
18
30
  const Io = Schema.Struct({
@@ -26,7 +38,7 @@ const Engine = Schema.Struct({
26
38
  kind: Schema.String,
27
39
  message: Schema.String
28
40
  })
29
- export const DbReason = Schema.Union([ResourceLimit, Io, Engine, PlainReason])
41
+ export const DbReason = Schema.Union([ResourceLimit, Io, Engine, InvalidArgument, PlainReason])
30
42
  export class DbError extends Schema.TaggedError<DbError>()("DbError", {
31
43
  operation: Schema.String,
32
44
  reason: DbReason
@@ -34,12 +46,44 @@ export class DbError extends Schema.TaggedError<DbError>()("DbError", {
34
46
  get code() {
35
47
  return this.reason._tag
36
48
  }
49
+ /** Safe default display; callers explicitly inspect structured detail. */
50
+ override get message(): string {
51
+ return `${this.operation}: ${this.code}`
52
+ }
37
53
  }
38
54
 
39
55
  const decodeReason = Schema.decodeUnknownOption(DbReason)
40
56
  export function dbError(operation: string, cause: unknown): DbError {
57
+ if (cause instanceof DbError) return cause
58
+ if (cause instanceof AuthoringError) return argumentError(operation, cause)
41
59
  const decoded = decodeReason(cause)
42
- return new DbError({ operation, reason: decoded._tag === "Some" ? decoded.value : { _tag: "Internal" } })
60
+ if (decoded._tag === "Some") return new DbError({ operation, reason: decoded.value })
61
+ // Synchronous native schema/marshalling refusals precede the executor's
62
+ // tagged error wire. Preserve their engine family instead of losing it.
63
+ if (
64
+ typeof cause === "object" &&
65
+ cause !== null &&
66
+ "kind" in cause &&
67
+ typeof cause.kind === "string" &&
68
+ "message" in cause &&
69
+ typeof cause.message === "string"
70
+ ) {
71
+ return new DbError({ operation, reason: { _tag: "Engine", kind: cause.kind, message: cause.message } })
72
+ }
73
+ return new DbError({ operation, reason: { _tag: "Internal" } })
74
+ }
75
+
76
+ /** Host input failures retain authoring details without displaying values. */
77
+ export function argumentError(operation: string, cause: unknown): DbError {
78
+ if (cause instanceof DbError) return cause
79
+ if (cause instanceof AuthoringError) {
80
+ const detail =
81
+ cause.diagnostic === undefined
82
+ ? { message: cause.message }
83
+ : { message: cause.message, diagnostic: cause.diagnostic }
84
+ return new DbError({ operation, reason: { _tag: "InvalidArgument", detail } })
85
+ }
86
+ return new DbError({ operation, reason: { _tag: "InvalidArgument" } })
43
87
  }
44
88
 
45
89
  const Outstanding = Schema.Struct({
@@ -49,11 +93,7 @@ const Outstanding = Schema.Struct({
49
93
  retained: Schema.BigInt,
50
94
  owners: Schema.BigInt,
51
95
  databases: Schema.BigInt,
52
- natives: Schema.BigInt,
53
- inputBytes: Schema.BigInt,
54
- workingBytes: Schema.BigInt,
55
- scratchBytes: Schema.BigInt,
56
- resultBytes: Schema.BigInt
96
+ natives: Schema.BigInt
57
97
  })
58
98
  export type OutstandingWork = typeof Outstanding.Type
59
99
  const Close = Schema.Union([