@bjornpagen/bumbledb 0.20.2 → 1.0.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 (192) hide show
  1. package/COOKBOOK.md +283 -1611
  2. package/README.md +156 -118
  3. package/dist/capacity.d.ts +16 -12
  4. package/dist/capacity.d.ts.map +1 -1
  5. package/dist/capacity.js +16 -48
  6. package/dist/capacity.js.map +1 -1
  7. package/dist/changes.d.ts +54 -0
  8. package/dist/changes.d.ts.map +1 -0
  9. package/dist/changes.js +245 -0
  10. package/dist/changes.js.map +1 -0
  11. package/dist/close.d.ts +24 -0
  12. package/dist/close.d.ts.map +1 -0
  13. package/dist/close.js +34 -0
  14. package/dist/close.js.map +1 -0
  15. package/dist/closed.d.ts +1 -3
  16. package/dist/closed.d.ts.map +1 -1
  17. package/dist/closed.js +24 -12
  18. package/dist/closed.js.map +1 -1
  19. package/dist/codec.d.ts +56 -0
  20. package/dist/codec.d.ts.map +1 -0
  21. package/dist/codec.js +289 -0
  22. package/dist/codec.js.map +1 -0
  23. package/dist/compile.d.ts +68 -0
  24. package/dist/compile.d.ts.map +1 -0
  25. package/dist/compile.js +75 -0
  26. package/dist/compile.js.map +1 -0
  27. package/dist/db-native.d.ts +170 -0
  28. package/dist/db-native.d.ts.map +1 -0
  29. package/dist/db-native.js +5 -0
  30. package/dist/db-native.js.map +1 -0
  31. package/dist/db.d.ts +101 -233
  32. package/dist/db.d.ts.map +1 -1
  33. package/dist/db.js +232 -1084
  34. package/dist/db.js.map +1 -1
  35. package/dist/errors.d.ts +57 -0
  36. package/dist/errors.d.ts.map +1 -0
  37. package/dist/errors.js +32 -0
  38. package/dist/errors.js.map +1 -0
  39. package/dist/face.d.ts.map +1 -1
  40. package/dist/face.js +2 -2
  41. package/dist/face.js.map +1 -1
  42. package/dist/fields.d.ts +65 -24
  43. package/dist/fields.d.ts.map +1 -1
  44. package/dist/fields.js +94 -24
  45. package/dist/fields.js.map +1 -1
  46. package/dist/index.d.ts +37 -32
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +17 -21
  49. package/dist/index.js.map +1 -1
  50. package/dist/internal/log.d.ts +24 -0
  51. package/dist/internal/log.d.ts.map +1 -0
  52. package/dist/internal/log.js +10 -0
  53. package/dist/internal/log.js.map +1 -0
  54. package/dist/law.d.ts +8 -13
  55. package/dist/law.d.ts.map +1 -1
  56. package/dist/law.js +6 -59
  57. package/dist/law.js.map +1 -1
  58. package/dist/lower.d.ts +0 -13
  59. package/dist/lower.d.ts.map +1 -1
  60. package/dist/lower.js +9 -17
  61. package/dist/lower.js.map +1 -1
  62. package/dist/migration.d.ts +33 -0
  63. package/dist/migration.d.ts.map +1 -0
  64. package/dist/migration.js +48 -0
  65. package/dist/migration.js.map +1 -0
  66. package/dist/native.d.ts +256 -301
  67. package/dist/native.d.ts.map +1 -1
  68. package/dist/native.js +65 -108
  69. package/dist/native.js.map +1 -1
  70. package/dist/query/atom.d.ts +24 -13
  71. package/dist/query/atom.d.ts.map +1 -1
  72. package/dist/query/atom.js +7 -3
  73. package/dist/query/atom.js.map +1 -1
  74. package/dist/query/compute.d.ts +68 -0
  75. package/dist/query/compute.d.ts.map +1 -0
  76. package/dist/query/compute.js +141 -0
  77. package/dist/query/compute.js.map +1 -0
  78. package/dist/query/find.d.ts +15 -8
  79. package/dist/query/find.d.ts.map +1 -1
  80. package/dist/query/find.js +6 -2
  81. package/dist/query/find.js.map +1 -1
  82. package/dist/query/lower.d.ts +23 -2
  83. package/dist/query/lower.d.ts.map +1 -1
  84. package/dist/query/lower.js +433 -80
  85. package/dist/query/lower.js.map +1 -1
  86. package/dist/query/parse-ir.d.ts.map +1 -1
  87. package/dist/query/parse-ir.js +17 -8
  88. package/dist/query/parse-ir.js.map +1 -1
  89. package/dist/query/run.d.ts +12 -2
  90. package/dist/query/run.d.ts.map +1 -1
  91. package/dist/query/run.js +26 -9
  92. package/dist/query/run.js.map +1 -1
  93. package/dist/query/scope.d.ts +47 -3
  94. package/dist/query/scope.d.ts.map +1 -1
  95. package/dist/query/scope.js +83 -19
  96. package/dist/query/scope.js.map +1 -1
  97. package/dist/relation.d.ts +3 -8
  98. package/dist/relation.d.ts.map +1 -1
  99. package/dist/relation.js +16 -10
  100. package/dist/relation.js.map +1 -1
  101. package/dist/result.d.ts +47 -0
  102. package/dist/result.d.ts.map +1 -0
  103. package/dist/result.js +50 -0
  104. package/dist/result.js.map +1 -0
  105. package/dist/rows.d.ts +69 -0
  106. package/dist/rows.d.ts.map +1 -0
  107. package/dist/rows.js +328 -0
  108. package/dist/rows.js.map +1 -0
  109. package/dist/runtime-codes.d.ts +2 -0
  110. package/dist/runtime-codes.d.ts.map +1 -0
  111. package/dist/runtime-codes.js +19 -0
  112. package/dist/runtime-codes.js.map +1 -0
  113. package/dist/runtime-errors.d.ts +106 -0
  114. package/dist/runtime-errors.d.ts.map +1 -0
  115. package/dist/runtime-errors.js +62 -0
  116. package/dist/runtime-errors.js.map +1 -0
  117. package/dist/runtime-native.d.ts +171 -0
  118. package/dist/runtime-native.d.ts.map +1 -0
  119. package/dist/runtime-native.js +5 -0
  120. package/dist/runtime-native.js.map +1 -0
  121. package/dist/runtime.d.ts +73 -0
  122. package/dist/runtime.d.ts.map +1 -0
  123. package/dist/runtime.js +293 -0
  124. package/dist/runtime.js.map +1 -0
  125. package/dist/scalar.d.ts +165 -0
  126. package/dist/scalar.d.ts.map +1 -0
  127. package/dist/scalar.js +344 -0
  128. package/dist/scalar.js.map +1 -0
  129. package/dist/schema.d.ts +1 -1
  130. package/dist/schema.d.ts.map +1 -1
  131. package/dist/schema.js +34 -37
  132. package/dist/schema.js.map +1 -1
  133. package/dist/shape.d.ts +28 -0
  134. package/dist/shape.d.ts.map +1 -0
  135. package/dist/shape.js +2 -0
  136. package/dist/shape.js.map +1 -0
  137. package/dist/spec.d.ts +27 -3
  138. package/dist/spec.d.ts.map +1 -1
  139. package/dist/spec.js +22 -1
  140. package/dist/spec.js.map +1 -1
  141. package/dist/statements.d.ts +16 -3
  142. package/dist/statements.d.ts.map +1 -1
  143. package/dist/statements.js +54 -28
  144. package/dist/statements.js.map +1 -1
  145. package/dist/uuid.d.ts +23 -0
  146. package/dist/uuid.d.ts.map +1 -0
  147. package/dist/uuid.js +54 -0
  148. package/dist/uuid.js.map +1 -0
  149. package/pack-provenance.json +7 -0
  150. package/package.json +12 -16
  151. package/src/capacity.ts +34 -54
  152. package/src/changes.ts +361 -0
  153. package/src/close.ts +53 -0
  154. package/src/closed.ts +25 -19
  155. package/src/codec.ts +379 -0
  156. package/src/compile.ts +149 -0
  157. package/src/db-native.ts +247 -0
  158. package/src/db.ts +461 -1632
  159. package/src/errors.ts +50 -0
  160. package/src/face.ts +2 -2
  161. package/src/fields.ts +182 -55
  162. package/src/index.ts +62 -126
  163. package/src/internal/log.ts +51 -0
  164. package/src/law.ts +13 -28
  165. package/src/lower.ts +9 -4
  166. package/src/migration.ts +69 -0
  167. package/src/native.ts +326 -487
  168. package/src/query/atom.ts +61 -25
  169. package/src/query/compute.ts +237 -0
  170. package/src/query/find.ts +31 -12
  171. package/src/query/lower.ts +531 -143
  172. package/src/query/parse-ir.ts +18 -13
  173. package/src/query/run.ts +32 -16
  174. package/src/query/scope.ts +140 -8
  175. package/src/relation.ts +17 -21
  176. package/src/result.ts +130 -0
  177. package/src/rows.ts +404 -0
  178. package/src/runtime-codes.ts +18 -0
  179. package/src/runtime-errors.ts +69 -0
  180. package/src/runtime-native.ts +242 -0
  181. package/src/runtime.ts +440 -0
  182. package/src/scalar.ts +560 -0
  183. package/src/schema.ts +35 -37
  184. package/src/shape.ts +31 -0
  185. package/src/spec.ts +33 -3
  186. package/src/statements.ts +75 -68
  187. package/src/uuid.ts +69 -0
  188. package/dist/marshal.d.ts +0 -44
  189. package/dist/marshal.d.ts.map +0 -1
  190. package/dist/marshal.js +0 -165
  191. package/dist/marshal.js.map +0 -1
  192. package/src/marshal.ts +0 -200
package/src/codec.ts ADDED
@@ -0,0 +1,379 @@
1
+ import { Effect, Schema as EffectSchema, Result } from "effect"
2
+ import { schemaTables } from "#compile.ts"
3
+ import { dbNative } from "#db-native.ts"
4
+ import type { AnyField } from "#fields.ts"
5
+ import { isFloatIntervalValue, isIntervalValue, rosterOf } from "#fields.ts"
6
+ import { lower } from "#lower.ts"
7
+ import type { AnyRelation, Fact } from "#relation.ts"
8
+ import type { CellValue } from "#rows.ts"
9
+ import { factOfCells, flatRowsOf } from "#rows.ts"
10
+ import type { ExecutionPolicy } from "#runtime.ts"
11
+ import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
12
+ import { DbError } from "#runtime-errors.ts"
13
+ import type { AnySchema } from "#schema.ts"
14
+ import type { Rel } from "#shape.ts"
15
+ import { f64BitsHex } from "#spec.ts"
16
+ import { Uuid } from "#uuid.ts"
17
+
18
+ /**
19
+ * Boundary row codecs, derived from the core relation descriptors — never a
20
+ * second hand-maintained field roster (chapter 35).
21
+ *
22
+ * Two layers:
23
+ *
24
+ * 1. The CANONICAL native row codec: `encodeRows`/`decodeRows` run the
25
+ * same charged native implementation the engine, log and migrations
26
+ * share (owned bytes in, owned typed rows out; untrusted input cannot
27
+ * inject native capabilities). Effect-only, `NativeRuntime` required.
28
+ * 2. The schema-tagged JSON VALUE form for HTTP/export boundaries
29
+ * (chapter 30): every `f64` — finite included — is
30
+ * `{"$f64":"<16 lowercase hex digits>"}` of canonical binary64 bits;
31
+ * integers are canonical decimal strings; `Uuid` is canonical UUID;
32
+ * bytes use ONE strict lowercase-hex encoding; intervals are
33
+ * `{start,end}` in their element encoding; closed references are handle
34
+ * names. `JSON.stringify` of raw numbers is NOT the database value
35
+ * wire codec — it loses infinities/NaN and cannot encode BigInt.
36
+ * Decoders reject malformed widths, unknown tags and noncanonical
37
+ * representations. These are pure bounded per-row functions.
38
+ */
39
+
40
+ /** The typed row shape: a schema plus one of its relations. */
41
+ interface RowShape<R extends AnyRelation> {
42
+ readonly schema: AnySchema
43
+ readonly relation: R
44
+ }
45
+
46
+ function rowShape<S extends AnySchema, R extends Rel<S>>(schema: S, relation: R): RowShape<R> {
47
+ return Object.freeze({ schema, relation })
48
+ }
49
+
50
+ function invalid(operation: string): DbError {
51
+ return new DbError({ operation, reason: { _tag: "InvalidArgument" } })
52
+ }
53
+
54
+ const DECIMAL = /^-?(?:0|[1-9][0-9]*)$/
55
+ const HEX16 = /^[0-9a-f]{16}$/
56
+ const LOWER_HEX = /^(?:[0-9a-f]{2})*$/
57
+
58
+ function hexOfBytes(bytes: Uint8Array): string {
59
+ let out = ""
60
+ for (const byte of bytes) {
61
+ out += byte.toString(16).padStart(2, "0")
62
+ }
63
+ return out
64
+ }
65
+
66
+ function bytesOfHex(text: string): Uint8Array | undefined {
67
+ if (!LOWER_HEX.test(text)) {
68
+ return undefined
69
+ }
70
+ const out = new Uint8Array(text.length / 2)
71
+ for (let index = 0; index < out.length; index += 1) {
72
+ out[index] = Number.parseInt(text.slice(index * 2, index * 2 + 2), 16)
73
+ }
74
+ return out
75
+ }
76
+
77
+ function f64OfHex(text: unknown): number | undefined {
78
+ if (typeof text !== "string" || !HEX16.test(text)) {
79
+ return undefined
80
+ }
81
+ const bits = BigInt(`0x${text}`)
82
+ const image = new DataView(new ArrayBuffer(8))
83
+ image.setBigUint64(0, bits)
84
+ const value = image.getFloat64(0)
85
+ // Canonical only: the one NaN bit pattern, and no negative zero image.
86
+ if (f64BitsHex(value) !== text) {
87
+ return undefined
88
+ }
89
+ return value
90
+ }
91
+
92
+ /** One host value to its schema-tagged JSON form (pure, bounded). */
93
+ function encodeBoundaryValue(field: AnyField, value: unknown): unknown {
94
+ const roster = rosterOf(field)
95
+ if (roster !== undefined) {
96
+ if (typeof value !== "string" || !roster.handles.includes(value)) {
97
+ throw invalid("encodeBoundaryRows")
98
+ }
99
+ return value
100
+ }
101
+ switch (field.kind) {
102
+ case "bool": {
103
+ if (typeof value !== "boolean") {
104
+ throw invalid("encodeBoundaryRows")
105
+ }
106
+ return value
107
+ }
108
+ case "u64":
109
+ case "i64": {
110
+ if (typeof value !== "bigint") {
111
+ throw invalid("encodeBoundaryRows")
112
+ }
113
+ return value.toString(10)
114
+ }
115
+ case "f64": {
116
+ if (typeof value !== "number") {
117
+ throw invalid("encodeBoundaryRows")
118
+ }
119
+ return { $f64: f64BitsHex(value) }
120
+ }
121
+ case "uuid": {
122
+ if (!Uuid.isUuid(value)) {
123
+ throw invalid("encodeBoundaryRows")
124
+ }
125
+ return value
126
+ }
127
+ case "str": {
128
+ if (typeof value !== "string" || !value.isWellFormed()) {
129
+ throw invalid("encodeBoundaryRows")
130
+ }
131
+ return value
132
+ }
133
+ case "bytes": {
134
+ if (!(value instanceof Uint8Array) || value.byteLength !== field.width) {
135
+ throw invalid("encodeBoundaryRows")
136
+ }
137
+ return hexOfBytes(value)
138
+ }
139
+ case "interval": {
140
+ if (field.element === "f64") {
141
+ if (!isFloatIntervalValue(value)) {
142
+ throw invalid("encodeBoundaryRows")
143
+ }
144
+ return { start: { $f64: f64BitsHex(value.start) }, end: { $f64: f64BitsHex(value.end) } }
145
+ }
146
+ if (!isIntervalValue(value)) {
147
+ throw invalid("encodeBoundaryRows")
148
+ }
149
+ return { start: value.start.toString(10), end: value.end.toString(10) }
150
+ }
151
+ }
152
+ }
153
+
154
+ const U64_MAX = 0xffffffffffffffffn
155
+ const I64_MIN = -0x8000000000000000n
156
+ const I64_MAX = 0x7fffffffffffffffn
157
+
158
+ function decodeInteger(kind: "u64" | "i64", value: unknown): bigint | undefined {
159
+ if (typeof value !== "string" || !DECIMAL.test(value)) {
160
+ return undefined
161
+ }
162
+ const parsed = BigInt(value)
163
+ if (kind === "u64" && (parsed < 0n || parsed > U64_MAX)) {
164
+ return undefined
165
+ }
166
+ if (kind === "i64" && (parsed < I64_MIN || parsed > I64_MAX)) {
167
+ return undefined
168
+ }
169
+ // Canonical decimal only (no leading zeros, no "-0") — DECIMAL enforces.
170
+ return parsed
171
+ }
172
+
173
+ function decodeTaggedF64(value: unknown): number | undefined {
174
+ if (typeof value !== "object" || value === null || !("$f64" in value)) {
175
+ return undefined
176
+ }
177
+ const keys = Object.keys(value)
178
+ if (keys.length !== 1) {
179
+ return undefined
180
+ }
181
+ return f64OfHex((value as { readonly $f64: unknown }).$f64)
182
+ }
183
+
184
+ /** One schema-tagged JSON form back to the host value (strict, pure). */
185
+ function decodeBoundaryValue(field: AnyField, value: unknown): unknown | undefined {
186
+ const roster = rosterOf(field)
187
+ if (roster !== undefined) {
188
+ return typeof value === "string" && roster.handles.includes(value) ? value : undefined
189
+ }
190
+ switch (field.kind) {
191
+ case "bool":
192
+ return typeof value === "boolean" ? value : undefined
193
+ case "u64":
194
+ case "i64":
195
+ return decodeInteger(field.kind, value)
196
+ case "f64":
197
+ return decodeTaggedF64(value)
198
+ case "uuid":
199
+ return Uuid.isUuid(value) ? value : undefined
200
+ case "str":
201
+ return typeof value === "string" && value.isWellFormed() ? value : undefined
202
+ case "bytes": {
203
+ if (typeof value !== "string" || value.length !== field.width * 2) {
204
+ return undefined
205
+ }
206
+ return bytesOfHex(value)
207
+ }
208
+ case "interval": {
209
+ if (typeof value !== "object" || value === null || !("start" in value) || !("end" in value)) {
210
+ return undefined
211
+ }
212
+ const raw = value as { readonly start: unknown; readonly end: unknown }
213
+ if (field.element === "f64") {
214
+ const start = decodeTaggedF64(raw.start)
215
+ const end = decodeTaggedF64(raw.end)
216
+ if (start === undefined || end === undefined || Number.isNaN(start) || Number.isNaN(end) || !(start < end)) {
217
+ return undefined
218
+ }
219
+ return Object.freeze({ start, end })
220
+ }
221
+ const start = decodeInteger(field.element, raw.start)
222
+ const end = decodeInteger(field.element, raw.end)
223
+ if (start === undefined || end === undefined || start >= end) {
224
+ return undefined
225
+ }
226
+ return Object.freeze({ start, end })
227
+ }
228
+ }
229
+ }
230
+
231
+ /** Pure schema-tagged JSON encoding of owned rows (bounded per call). */
232
+ function encodeBoundaryRows<R extends AnyRelation>(
233
+ relation: R,
234
+ rows: Iterable<Fact<R>>
235
+ ): Result.Result<ReadonlyArray<Readonly<Record<string, unknown>>>, DbError> {
236
+ return Result.try({
237
+ try: () => {
238
+ const out: Array<Readonly<Record<string, unknown>>> = []
239
+ for (const row of rows) {
240
+ const record: Record<string, unknown> = {}
241
+ for (const declared of relation.data.fields) {
242
+ record[declared.name] = encodeBoundaryValue(
243
+ declared.field,
244
+ (row as Readonly<Record<string, unknown>>)[declared.name]
245
+ )
246
+ }
247
+ out.push(Object.freeze(record))
248
+ }
249
+ return Object.freeze(out)
250
+ },
251
+ catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeBoundaryRows"))
252
+ })
253
+ }
254
+
255
+ /** Pure strict decoding of schema-tagged JSON rows; any refusal is typed. */
256
+ function decodeBoundaryRows<R extends AnyRelation>(
257
+ relation: R,
258
+ input: unknown
259
+ ): Result.Result<ReadonlyArray<Fact<R>>, DbError> {
260
+ if (!Array.isArray(input)) {
261
+ return Result.fail(invalid("decodeBoundaryRows"))
262
+ }
263
+ const out: Array<Fact<R>> = []
264
+ for (const raw of input) {
265
+ if (typeof raw !== "object" || raw === null) {
266
+ return Result.fail(invalid("decodeBoundaryRows"))
267
+ }
268
+ const record: Record<string, unknown> = {}
269
+ for (const declared of relation.data.fields) {
270
+ const decoded = decodeBoundaryValue(declared.field, (raw as Readonly<Record<string, unknown>>)[declared.name])
271
+ if (decoded === undefined) {
272
+ return Result.fail(invalid("decodeBoundaryRows"))
273
+ }
274
+ record[declared.name] = decoded
275
+ }
276
+ // Unknown extra keys refuse: wrong-schema values never pass.
277
+ for (const key of Object.keys(raw)) {
278
+ if (!(key in record)) {
279
+ return Result.fail(invalid("decodeBoundaryRows"))
280
+ }
281
+ }
282
+ out.push(Object.freeze(record) as Fact<R>)
283
+ }
284
+ return Result.succeed(Object.freeze(out))
285
+ }
286
+
287
+ /**
288
+ * An Effect Schema for one relation's typed row, DERIVED from the core
289
+ * descriptors (a validation-only declaration; the canonical `$f64`/decimal
290
+ * wire form stays this module's explicit codec — Effect Schema's generic
291
+ * JSON number encoding is not Bumbledb's `$f64` codec).
292
+ */
293
+ function rowSchema<R extends AnyRelation>(relation: R) {
294
+ return EffectSchema.declare((value: unknown): value is Fact<R> => {
295
+ if (typeof value !== "object" || value === null) {
296
+ return false
297
+ }
298
+ const record = value as Readonly<Record<string, unknown>>
299
+ return relation.data.fields.every(function checkField(declared) {
300
+ const cell = record[declared.name]
301
+ if (cell === undefined) {
302
+ return false
303
+ }
304
+ return Result.isSuccess(
305
+ Result.try({
306
+ try: () => encodeBoundaryValue(declared.field, cell),
307
+ catch: () => invalid("rowSchema")
308
+ })
309
+ )
310
+ })
311
+ })
312
+ }
313
+
314
+ /**
315
+ * The charged CANONICAL native row codec (chapter 35 roster): owned bytes
316
+ * out, owned typed rows back in — the same implementation log sealing and
317
+ * migrations use. Binding parameters is ingestion: input must stay stable
318
+ * through execution, and no native work starts before the checked owned
319
+ * chunk exists.
320
+ */
321
+ const encodeRows = Effect.fn("encodeRows")(function* <R extends AnyRelation>(
322
+ shape: RowShape<R>,
323
+ rows: Iterable<Fact<R>>,
324
+ work: ExecutionPolicy
325
+ ) {
326
+ const runtime = yield* runtimeHandle()
327
+ const tables = schemaTables(shape.schema)
328
+ const relationId = tables.relationIds.get(shape.relation.name)
329
+ if (relationId === undefined || shape.schema.relations[shape.relation.name] !== shape.relation) {
330
+ return yield* Effect.fail(invalid("encodeRows"))
331
+ }
332
+ const flat = yield* Effect.try({
333
+ try: () => flatRowsOf(shape.relation.data, rows as Iterable<object>),
334
+ catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeRows"))
335
+ })
336
+ const spec = lower(shape.schema)
337
+ return yield* nativeOperationWith(
338
+ "encodeRows",
339
+ (callback) =>
340
+ dbNative.runtimeEncodeRows(
341
+ runtime,
342
+ policyWire(work, "encodeRows"),
343
+ spec,
344
+ relationId,
345
+ flat.rows,
346
+ flat.cells,
347
+ callback
348
+ ),
349
+ dbNative.runtimeBytesTake,
350
+ (bytes) => bytes
351
+ )
352
+ })
353
+
354
+ const decodeRows = Effect.fn("decodeRows")(function* <R extends AnyRelation>(
355
+ shape: RowShape<R>,
356
+ input: Uint8Array,
357
+ work: ExecutionPolicy
358
+ ) {
359
+ const runtime = yield* runtimeHandle()
360
+ const tables = schemaTables(shape.schema)
361
+ const relationId = tables.relationIds.get(shape.relation.name)
362
+ if (relationId === undefined || shape.schema.relations[shape.relation.name] !== shape.relation) {
363
+ return yield* Effect.fail(invalid("decodeRows"))
364
+ }
365
+ if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer)) {
366
+ return yield* Effect.fail(invalid("decodeRows"))
367
+ }
368
+ const spec = lower(shape.schema)
369
+ return yield* nativeOperationWith(
370
+ "decodeRows",
371
+ (callback) =>
372
+ dbNative.runtimeDecodeRows(runtime, policyWire(work, "decodeRows"), spec, relationId, input, callback),
373
+ dbNative.runtimeRowsTake,
374
+ (rows) => Object.freeze(rows.map((row) => factOfCells(shape.relation, row as readonly CellValue[])))
375
+ )
376
+ })
377
+
378
+ export type { RowShape }
379
+ export { decodeBoundaryRows, decodeRows, encodeBoundaryRows, encodeRows, rowSchema, rowShape }
package/src/compile.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { Effect } from "effect"
2
+ import { isClosedMember } from "#closed.ts"
3
+ import { dbNative } from "#db-native.ts"
4
+ import { SdkInvariantError } from "#errors.ts"
5
+ import type { SchemaClasses } from "#law.ts"
6
+ import { lower } from "#lower.ts"
7
+ import type { SealedDescriptor } from "#native.ts"
8
+ import type { ExecutionPolicy } from "#runtime.ts"
9
+ import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
10
+ import type { AnySchema, Schema as SchemaDeclaration, SchemaRelations } from "#schema.ts"
11
+ import type { Statement } from "#statements.ts"
12
+
13
+ /**
14
+ * `SchemaId` — the canonical schema/theory identity (chapter 30's value
15
+ * vocabulary): the engine's canonical schema fingerprint as lowercase hex.
16
+ * Independent of database identity; a branded string so arbitrary text does
17
+ * not typecheck where a schema identity is required.
18
+ */
19
+ declare const schemaIdBrand: unique symbol
20
+
21
+ type SchemaId = string & { readonly [schemaIdBrand]: "bumbledb.SchemaId" }
22
+
23
+ /**
24
+ * `CompiledSchema<S>` — bounded detached immutable descriptor data plus the
25
+ * canonical `schemaId` (chapter 35). NOT a tenant handle or a second
26
+ * user-authored schema: it needs no native finalizer, holds no native
27
+ * resource, and open/create/build compile through the same implementation —
28
+ * prior compilation is optional, never a mandatory prepare ceremony.
29
+ */
30
+ interface CompiledSchema<S extends AnySchema> {
31
+ readonly schema: S
32
+ readonly schemaId: SchemaId
33
+ readonly descriptor: SealedDescriptor
34
+ }
35
+
36
+ /**
37
+ * Pure declaration-order tables shared by the whole core surface: relation
38
+ * ids (declaration order = ids), materialized statement ids (closed
39
+ * relations' auto-handle keys FIRST in relation-declaration order, then
40
+ * declared statements in declaration order with `mirrors` occupying two
41
+ * consecutive slots, source-first — the theory's `StatementId` law), and
42
+ * each relation's primary key (a closed relation's synthetic `(id)`,
43
+ * otherwise its FIRST declared `key` statement).
44
+ */
45
+ interface PrimaryKey {
46
+ readonly statementId: number
47
+ readonly projection: readonly string[]
48
+ }
49
+
50
+ interface SchemaTables {
51
+ readonly relationIds: ReadonlyMap<string, number>
52
+ readonly primaryKeys: ReadonlyMap<string, PrimaryKey>
53
+ }
54
+
55
+ function declaredWidth(statement: Statement): number {
56
+ return statement.data.kind === "mirrors" ? 2 : 1
57
+ }
58
+
59
+ function tablesOf(theory: AnySchema): SchemaTables {
60
+ const relationIds = new Map<string, number>()
61
+ const primaryKeys = new Map<string, PrimaryKey>()
62
+ let autoKeys = 0
63
+ Object.entries(theory.relations).forEach(function assignRelation([name, member], ordinal) {
64
+ relationIds.set(name, ordinal)
65
+ if (isClosedMember(member)) {
66
+ primaryKeys.set(name, Object.freeze({ statementId: autoKeys, projection: Object.freeze(["id"]) }))
67
+ autoKeys += 1
68
+ }
69
+ })
70
+ let offset = autoKeys
71
+ for (const statement of theory.statements) {
72
+ const data = statement.data
73
+ if (data.kind === "key" && !primaryKeys.has(data.owner.name)) {
74
+ primaryKeys.set(data.owner.name, Object.freeze({ statementId: offset, projection: data.projection }))
75
+ }
76
+ offset += declaredWidth(statement)
77
+ }
78
+ return Object.freeze({ relationIds, primaryKeys })
79
+ }
80
+
81
+ const compiledCache = new WeakMap<AnySchema, SchemaTables>()
82
+
83
+ /** Memoized pure tables (per schema value identity; no native work). */
84
+ function schemaTables(theory: AnySchema): SchemaTables {
85
+ const cached = compiledCache.get(theory)
86
+ if (cached !== undefined) {
87
+ return cached
88
+ }
89
+ const built = tablesOf(theory)
90
+ compiledCache.set(theory, built)
91
+ return built
92
+ }
93
+
94
+ function admitSchemaId(fingerprint: string): SchemaId {
95
+ if (typeof fingerprint !== "string" || fingerprint.length === 0) {
96
+ throw new SdkInvariantError({ message: "Schema.compile: the engine returned no canonical fingerprint" })
97
+ }
98
+ return fingerprint as SchemaId
99
+ }
100
+
101
+ /**
102
+ * Charged, effectful native schema admission/compilation (chapter 35): a
103
+ * schema declaration is pure metadata and never a claim its theory has been
104
+ * admitted. Runs on the one bounded executor under `work`, requires the
105
+ * acquired {@link NativeRuntime}, and yields detached immutable descriptor
106
+ * data plus the canonical schema identity. No database is opened and no
107
+ * native finalizer is created.
108
+ */
109
+ const compile = Effect.fn("Schema.compile")(function* <S extends AnySchema>(schema: S, work: ExecutionPolicy) {
110
+ const handle = yield* runtimeHandle()
111
+ const spec = lower(schema)
112
+ const descriptor = yield* nativeOperationWith(
113
+ "Schema.compile",
114
+ (callback) => dbNative.runtimeSchemaCompile(handle, policyWire(work, "Schema.compile"), spec, callback),
115
+ dbNative.runtimeSchemaTake,
116
+ (value) => value
117
+ )
118
+ const compiled: CompiledSchema<S> = Object.freeze({
119
+ schema,
120
+ schemaId: admitSchemaId(descriptor.fingerprint),
121
+ descriptor
122
+ })
123
+ return compiled
124
+ })
125
+
126
+ /**
127
+ * The declaration-tier `Schema<Rels, Classes>` TYPE (from `#schema.ts`),
128
+ * re-aliased LOCALLY so the one name carries both meanings through a
129
+ * SINGLE export specifier below — a local `type` + `const` merge is the
130
+ * plain TypeScript type/value merge, with no same-name pair of export
131
+ * declarations for any checker to refuse (the earlier
132
+ * `export type { Schema } from "#schema.ts"` beside `export { Schema }`
133
+ * spelled the type meaning twice at the barrel; this spelling cannot).
134
+ */
135
+ type Schema<Rels extends SchemaRelations, Classes extends SchemaClasses = SchemaClasses> = SchemaDeclaration<
136
+ Rels,
137
+ Classes
138
+ >
139
+
140
+ /**
141
+ * The core `Schema` namespace (import as `BumbleSchema` when Effect Schema
142
+ * is also in scope — chapter 35). The `Schema<Rels>` TYPE remains the pure
143
+ * declaration from `#schema.ts` (the local alias above); this value owns
144
+ * the effectful half. One exported name, two meanings.
145
+ */
146
+ const Schema = Object.freeze({ compile })
147
+
148
+ export type { CompiledSchema, PrimaryKey, SchemaId, SchemaTables }
149
+ export { Schema, schemaTables }