@bjornpagen/bumbledb 1.1.0 → 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 (148) hide show
  1. package/COOKBOOK.md +114 -11
  2. package/README.md +52 -9
  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 +26 -3
  8. package/dist/changes.d.ts.map +1 -1
  9. package/dist/changes.js +89 -44
  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 +4 -9
  16. package/dist/codec.d.ts.map +1 -1
  17. package/dist/codec.js +108 -183
  18. package/dist/codec.js.map +1 -1
  19. package/dist/compile.d.ts +16 -16
  20. package/dist/compile.d.ts.map +1 -1
  21. package/dist/compile.js +36 -15
  22. package/dist/compile.js.map +1 -1
  23. package/dist/db-native.d.ts +39 -0
  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 +28 -8
  28. package/dist/db.d.ts.map +1 -1
  29. package/dist/db.js +91 -38
  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 +14 -10
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5 -3
  50. package/dist/index.js.map +1 -1
  51. package/dist/internal/log.d.ts +2 -0
  52. package/dist/internal/log.d.ts.map +1 -1
  53. package/dist/internal/log.js +2 -0
  54. package/dist/internal/log.js.map +1 -1
  55. package/dist/law.d.ts +10 -10
  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/native.d.ts +1 -1
  63. package/dist/native.d.ts.map +1 -1
  64. package/dist/native.js.map +1 -1
  65. package/dist/query/atom.d.ts +1 -1
  66. package/dist/query/atom.d.ts.map +1 -1
  67. package/dist/query/atom.js.map +1 -1
  68. package/dist/query/description.d.ts +33 -0
  69. package/dist/query/description.d.ts.map +1 -0
  70. package/dist/query/description.js +302 -0
  71. package/dist/query/description.js.map +1 -0
  72. package/dist/query/lower.d.ts +60 -4
  73. package/dist/query/lower.d.ts.map +1 -1
  74. package/dist/query/lower.js +126 -161
  75. package/dist/query/lower.js.map +1 -1
  76. package/dist/query/parse-ir.d.ts +7 -2
  77. package/dist/query/parse-ir.d.ts.map +1 -1
  78. package/dist/query/parse-ir.js +240 -53
  79. package/dist/query/parse-ir.js.map +1 -1
  80. package/dist/query/run.d.ts.map +1 -1
  81. package/dist/query/run.js +8 -10
  82. package/dist/query/run.js.map +1 -1
  83. package/dist/query/scope.d.ts +3 -4
  84. package/dist/query/scope.d.ts.map +1 -1
  85. package/dist/query/scope.js +5 -10
  86. package/dist/query/scope.js.map +1 -1
  87. package/dist/relation.d.ts +22 -48
  88. package/dist/relation.d.ts.map +1 -1
  89. package/dist/relation.js +30 -82
  90. package/dist/relation.js.map +1 -1
  91. package/dist/rows.d.ts +7 -4
  92. package/dist/rows.d.ts.map +1 -1
  93. package/dist/rows.js +44 -198
  94. package/dist/rows.js.map +1 -1
  95. package/dist/runtime-errors.d.ts +26 -2
  96. package/dist/runtime-errors.d.ts.map +1 -1
  97. package/dist/runtime-errors.js +43 -3
  98. package/dist/runtime-errors.js.map +1 -1
  99. package/dist/schema.d.ts +9 -1
  100. package/dist/schema.d.ts.map +1 -1
  101. package/dist/schema.js +85 -24
  102. package/dist/schema.js.map +1 -1
  103. package/dist/selection.d.ts +27 -0
  104. package/dist/selection.d.ts.map +1 -0
  105. package/dist/selection.js +104 -0
  106. package/dist/selection.js.map +1 -0
  107. package/dist/shape.d.ts +6 -9
  108. package/dist/shape.d.ts.map +1 -1
  109. package/dist/statements.d.ts +17 -52
  110. package/dist/statements.d.ts.map +1 -1
  111. package/dist/statements.js +92 -111
  112. package/dist/statements.js.map +1 -1
  113. package/dist/values.d.ts +25 -0
  114. package/dist/values.d.ts.map +1 -0
  115. package/dist/values.js +203 -0
  116. package/dist/values.js.map +1 -0
  117. package/pack-provenance.json +3 -3
  118. package/package.json +4 -4
  119. package/src/capacity.ts +86 -76
  120. package/src/changes.ts +183 -54
  121. package/src/closed.ts +161 -314
  122. package/src/codec.ts +116 -190
  123. package/src/compile.ts +56 -32
  124. package/src/db-native.ts +42 -0
  125. package/src/db.ts +134 -52
  126. package/src/errors.ts +16 -1
  127. package/src/face.ts +44 -74
  128. package/src/fields.ts +100 -146
  129. package/src/immutable.ts +76 -0
  130. package/src/index.ts +34 -28
  131. package/src/internal/log.ts +2 -0
  132. package/src/law.ts +13 -13
  133. package/src/lower.ts +13 -10
  134. package/src/native.ts +2 -0
  135. package/src/query/atom.ts +1 -0
  136. package/src/query/description.ts +394 -0
  137. package/src/query/lower.ts +139 -173
  138. package/src/query/parse-ir.ts +247 -54
  139. package/src/query/run.ts +15 -9
  140. package/src/query/scope.ts +10 -18
  141. package/src/relation.ts +58 -142
  142. package/src/rows.ts +62 -205
  143. package/src/runtime-errors.ts +47 -3
  144. package/src/schema.ts +99 -29
  145. package/src/selection.ts +124 -0
  146. package/src/shape.ts +9 -9
  147. package/src/statements.ts +138 -189
  148. package/src/values.ts +223 -0
package/src/codec.ts CHANGED
@@ -1,18 +1,20 @@
1
1
  import { Effect, Schema as EffectSchema, Result } from "effect"
2
+ import { membersAgree } from "#closed.ts"
2
3
  import { schemaTables } from "#compile.ts"
3
4
  import { dbNative } from "#db-native.ts"
5
+ import { AuthoringError } from "#errors.ts"
4
6
  import type { AnyField } from "#fields.ts"
5
- import { isFloatIntervalValue, isIntervalValue, rosterOf } from "#fields.ts"
6
7
  import { lower } from "#lower.ts"
7
- import type { AnyRelation, Fact } from "#relation.ts"
8
+ import { type AnyRelation, type Fact, relationDescriptor, relationFields } from "#relation.ts"
8
9
  import type { CellValue } from "#rows.ts"
9
10
  import { factOfCells, flatRowsOf } from "#rows.ts"
10
11
  import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
11
- import { DbError } from "#runtime-errors.ts"
12
+ import { argumentError, DbError } from "#runtime-errors.ts"
12
13
  import type { AnySchema } from "#schema.ts"
14
+ import { schemaDescriptor } from "#schema.ts"
13
15
  import type { Rel } from "#shape.ts"
14
16
  import { f64BitsHex } from "#spec.ts"
15
- import { Uuid } from "#uuid.ts"
17
+ import { fieldValue, recordValue } from "#values.ts"
16
18
 
17
19
  /**
18
20
  * Boundary row codecs, derived from the core relation descriptors — never a
@@ -43,14 +45,27 @@ interface RowShape<R extends AnyRelation> {
43
45
  }
44
46
 
45
47
  function rowShape<S extends AnySchema, R extends Rel<S>>(schema: S, relation: R): RowShape<R> {
46
- return Object.freeze({ schema, relation })
48
+ const ownedSchema = schemaDescriptor(schema)
49
+ const ownedRelation = relationDescriptor(relation)
50
+ return Object.freeze({ schema: ownedSchema, relation: ownedRelation })
51
+ }
52
+
53
+ function checkedRowShape<R extends AnyRelation>(operation: string, input: RowShape<R>) {
54
+ recordValue(`${operation} shape`, input, ["schema", "relation"])
55
+ const schema = schemaDescriptor(input.schema)
56
+ const relation = relationDescriptor(input.relation)
57
+ const relationId = schemaTables(schema).relationIds.get(relation.name)
58
+ if (relationId === undefined || !membersAgree(schema.relations[relation.name], relation)) {
59
+ throw new AuthoringError({ message: `${operation}: relation is not declared in the schema` })
60
+ }
61
+ return { relation, relationId, spec: lower(schema) }
47
62
  }
48
63
 
49
64
  function invalid(operation: string): DbError {
50
65
  return new DbError({ operation, reason: { _tag: "InvalidArgument" } })
51
66
  }
52
67
 
53
- const DECIMAL = /^-?(?:0|[1-9][0-9]*)$/
68
+ const DECIMAL = /^(?:0|-?[1-9][0-9]*)$/
54
69
  const HEX16 = /^[0-9a-f]{16}$/
55
70
  const LOWER_HEX = /^(?:[0-9a-f]{2})*$/
56
71
 
@@ -88,146 +103,71 @@ function f64OfHex(text: unknown): number | undefined {
88
103
  return value
89
104
  }
90
105
 
91
- /** One host value to its schema-tagged JSON form (pure, bounded). */
92
- function encodeBoundaryValue(field: AnyField, value: unknown): unknown {
93
- const roster = rosterOf(field)
94
- if (roster !== undefined) {
95
- if (typeof value !== "string" || !roster.handles.includes(value)) {
96
- throw invalid("encodeBoundaryRows")
97
- }
98
- return value
99
- }
106
+ /** One host value to its canonical, field-directed JSON representation. */
107
+ function encodeBoundaryValue(context: string, field: AnyField, input: unknown): unknown {
108
+ if ("closed" in field) return fieldValue(context, field, input)
100
109
  switch (field.kind) {
101
- case "bool": {
102
- if (typeof value !== "boolean") {
103
- throw invalid("encodeBoundaryRows")
104
- }
105
- return value
106
- }
110
+ case "bool":
111
+ case "str":
112
+ case "uuid":
113
+ return fieldValue(context, field, input)
107
114
  case "u64":
108
- case "i64": {
109
- if (typeof value !== "bigint") {
110
- throw invalid("encodeBoundaryRows")
111
- }
112
- return value.toString(10)
113
- }
114
- case "f64": {
115
- if (typeof value !== "number") {
116
- throw invalid("encodeBoundaryRows")
117
- }
118
- return { $f64: f64BitsHex(value) }
119
- }
120
- case "uuid": {
121
- if (!Uuid.isUuid(value)) {
122
- throw invalid("encodeBoundaryRows")
123
- }
124
- return value
125
- }
126
- case "str": {
127
- if (typeof value !== "string" || !value.isWellFormed()) {
128
- throw invalid("encodeBoundaryRows")
129
- }
130
- return value
131
- }
132
- case "bytes": {
133
- if (!(value instanceof Uint8Array) || value.byteLength !== field.width) {
134
- throw invalid("encodeBoundaryRows")
135
- }
136
- return hexOfBytes(value)
137
- }
115
+ case "i64":
116
+ return fieldValue(context, field, input).toString(10)
117
+ case "f64":
118
+ return { $f64: f64BitsHex(fieldValue(context, field, input)) }
119
+ case "bytes":
120
+ return hexOfBytes(fieldValue(context, field, input))
138
121
  case "interval": {
139
122
  if (field.element === "f64") {
140
- if (!isFloatIntervalValue(value)) {
141
- throw invalid("encodeBoundaryRows")
142
- }
123
+ const value = fieldValue(context, { ...field, element: "f64" }, input)
143
124
  return { start: { $f64: f64BitsHex(value.start) }, end: { $f64: f64BitsHex(value.end) } }
144
125
  }
145
- if (!isIntervalValue(value)) {
146
- throw invalid("encodeBoundaryRows")
147
- }
126
+ const value = fieldValue(context, { ...field, element: field.element }, input)
148
127
  return { start: value.start.toString(10), end: value.end.toString(10) }
149
128
  }
150
129
  }
151
130
  }
152
131
 
153
- const U64_MAX = 0xffffffffffffffffn
154
- const I64_MIN = -0x8000000000000000n
155
- const I64_MAX = 0x7fffffffffffffffn
156
-
157
- function decodeInteger(kind: "u64" | "i64", value: unknown): bigint | undefined {
158
- if (typeof value !== "string" || !DECIMAL.test(value)) {
159
- return undefined
160
- }
161
- const parsed = BigInt(value)
162
- if (kind === "u64" && (parsed < 0n || parsed > U64_MAX)) {
163
- return undefined
164
- }
165
- if (kind === "i64" && (parsed < I64_MIN || parsed > I64_MAX)) {
166
- return undefined
167
- }
168
- // Canonical decimal only (no leading zeros, no "-0") — DECIMAL enforces.
169
- return parsed
132
+ function decodeInteger(value: unknown): bigint | undefined {
133
+ // No accepted integer spelling is longer than 20 ASCII characters.
134
+ if (typeof value !== "string" || value.length > 20 || !DECIMAL.test(value)) return undefined
135
+ return BigInt(value)
170
136
  }
171
137
 
172
138
  function decodeTaggedF64(value: unknown): number | undefined {
173
- if (typeof value !== "object" || value === null || !("$f64" in value)) {
174
- return undefined
175
- }
176
- const keys = Object.keys(value)
177
- if (keys.length !== 1) {
178
- return undefined
179
- }
180
- return f64OfHex((value as { readonly $f64: unknown }).$f64)
139
+ const record = recordValue("binary64 image", value, ["$f64"])
140
+ return f64OfHex(record.$f64)
181
141
  }
182
142
 
183
- /** One schema-tagged JSON form back to the host value (strict, pure). */
184
- function decodeBoundaryValue(field: AnyField, value: unknown): unknown | undefined {
185
- const roster = rosterOf(field)
186
- if (roster !== undefined) {
187
- return typeof value === "string" && roster.handles.includes(value) ? value : undefined
188
- }
143
+ /** Decode representation, then judge the value through the shared interpreter. */
144
+ function decodeBoundaryValue(context: string, field: AnyField, input: unknown): unknown {
145
+ if ("closed" in field) return fieldValue(context, field, input)
146
+ let decoded: unknown = input
189
147
  switch (field.kind) {
190
- case "bool":
191
- return typeof value === "boolean" ? value : undefined
192
148
  case "u64":
193
149
  case "i64":
194
- return decodeInteger(field.kind, value)
150
+ decoded = decodeInteger(input)
151
+ break
195
152
  case "f64":
196
- return decodeTaggedF64(value)
197
- case "uuid":
198
- return Uuid.isUuid(value) ? value : undefined
199
- case "str":
200
- return typeof value === "string" && value.isWellFormed() ? value : undefined
201
- case "bytes": {
202
- if (typeof value !== "string" || value.length !== field.width * 2) {
203
- return undefined
204
- }
205
- return bytesOfHex(value)
206
- }
153
+ decoded = decodeTaggedF64(input)
154
+ break
155
+ case "bytes":
156
+ decoded = typeof input === "string" && input.length === field.width * 2 ? bytesOfHex(input) : undefined
157
+ break
207
158
  case "interval": {
208
- if (typeof value !== "object" || value === null || !("start" in value) || !("end" in value)) {
209
- return undefined
210
- }
211
- const raw = value as { readonly start: unknown; readonly end: unknown }
212
- if (field.element === "f64") {
213
- const start = decodeTaggedF64(raw.start)
214
- const end = decodeTaggedF64(raw.end)
215
- if (start === undefined || end === undefined || Number.isNaN(start) || Number.isNaN(end) || !(start < end)) {
216
- return undefined
217
- }
218
- return Object.freeze({ start, end })
219
- }
220
- const start = decodeInteger(field.element, raw.start)
221
- const end = decodeInteger(field.element, raw.end)
222
- if (start === undefined || end === undefined || start >= end) {
223
- return undefined
224
- }
225
- return Object.freeze({ start, end })
159
+ const record = recordValue(context, input, ["start", "end"])
160
+ decoded =
161
+ field.element === "f64"
162
+ ? { start: decodeTaggedF64(record.start), end: decodeTaggedF64(record.end) }
163
+ : { start: decodeInteger(record.start), end: decodeInteger(record.end) }
164
+ break
226
165
  }
227
166
  }
167
+ return fieldValue(context, field, decoded)
228
168
  }
229
169
 
230
- /** Pure schema-tagged JSON encoding of owned rows (bounded per call). */
170
+ /** Pure schema-tagged JSON encoding of complete rows. */
231
171
  function encodeBoundaryRows<R extends AnyRelation>(
232
172
  relation: R,
233
173
  rows: Iterable<Fact<R>>
@@ -235,78 +175,68 @@ function encodeBoundaryRows<R extends AnyRelation>(
235
175
  return Result.try({
236
176
  try: () => {
237
177
  const out: Array<Readonly<Record<string, unknown>>> = []
178
+ const fields = relationFields(relation)
179
+ const names = fields.map((declared) => declared.name)
238
180
  for (const row of rows) {
239
- const record: Record<string, unknown> = {}
240
- for (const declared of relation.data.fields) {
241
- record[declared.name] = encodeBoundaryValue(
242
- declared.field,
243
- (row as Readonly<Record<string, unknown>>)[declared.name]
181
+ const input = recordValue(`relation ${relation.name}`, row, names)
182
+ out.push(
183
+ Object.freeze(
184
+ Object.fromEntries(
185
+ fields.map((declared) => [
186
+ declared.name,
187
+ encodeBoundaryValue(`relation ${relation.name}.${declared.name}`, declared.field, input[declared.name])
188
+ ])
189
+ )
244
190
  )
245
- }
246
- out.push(Object.freeze(record))
191
+ )
247
192
  }
248
193
  return Object.freeze(out)
249
194
  },
250
- catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeBoundaryRows"))
195
+ catch: (cause) => argumentError("encodeBoundaryRows", cause)
251
196
  })
252
197
  }
253
198
 
254
- /** Pure strict decoding of schema-tagged JSON rows; any refusal is typed. */
199
+ /** Pure strict decoding; malformed records, getters and values fail as data. */
255
200
  function decodeBoundaryRows<R extends AnyRelation>(
256
201
  relation: R,
257
202
  input: unknown
258
203
  ): Result.Result<ReadonlyArray<Fact<R>>, DbError> {
259
- if (!Array.isArray(input)) {
260
- return Result.fail(invalid("decodeBoundaryRows"))
261
- }
262
- const out: Array<Fact<R>> = []
263
- for (const raw of input) {
264
- if (typeof raw !== "object" || raw === null) {
265
- return Result.fail(invalid("decodeBoundaryRows"))
266
- }
267
- const record: Record<string, unknown> = {}
268
- for (const declared of relation.data.fields) {
269
- const decoded = decodeBoundaryValue(declared.field, (raw as Readonly<Record<string, unknown>>)[declared.name])
270
- if (decoded === undefined) {
271
- return Result.fail(invalid("decodeBoundaryRows"))
272
- }
273
- record[declared.name] = decoded
274
- }
275
- // Unknown extra keys refuse: wrong-schema values never pass.
276
- for (const key of Object.keys(raw)) {
277
- if (!(key in record)) {
278
- return Result.fail(invalid("decodeBoundaryRows"))
279
- }
280
- }
281
- out.push(Object.freeze(record) as Fact<R>)
282
- }
283
- return Result.succeed(Object.freeze(out))
204
+ return Result.try({
205
+ try: () => {
206
+ if (!Array.isArray(input)) throw invalid("decodeBoundaryRows")
207
+ const fields = relationFields(relation)
208
+ const names = fields.map((declared) => declared.name)
209
+ return Object.freeze(
210
+ input.map((row) => {
211
+ const raw = recordValue(`relation ${relation.name}`, row, names)
212
+ return Object.freeze(
213
+ Object.fromEntries(
214
+ fields.map((declared) => [
215
+ declared.name,
216
+ decodeBoundaryValue(`relation ${relation.name}.${declared.name}`, declared.field, raw[declared.name])
217
+ ])
218
+ )
219
+ ) as Fact<R>
220
+ })
221
+ )
222
+ },
223
+ catch: (cause) => argumentError("decodeBoundaryRows", cause)
224
+ })
284
225
  }
285
226
 
286
- /**
287
- * An Effect Schema for one relation's typed row, DERIVED from the core
288
- * descriptors (a validation-only declaration; the canonical `$f64`/decimal
289
- * wire form stays this module's explicit codec — Effect Schema's generic
290
- * JSON number encoding is not Bumbledb's `$f64` codec).
291
- */
227
+ /** An Effect Schema derived from the same complete-row value interpreter. */
292
228
  function rowSchema<R extends AnyRelation>(relation: R) {
229
+ const fields = relationFields(relation)
230
+ const names = fields.map((declared) => declared.name)
293
231
  return EffectSchema.declare((value: unknown): value is Fact<R> => {
294
- if (typeof value !== "object" || value === null) {
232
+ try {
233
+ const record = recordValue(`relation ${relation.name}`, value, names)
234
+ for (const declared of fields)
235
+ fieldValue(`relation ${relation.name}.${declared.name}`, declared.field, record[declared.name])
236
+ return true
237
+ } catch {
295
238
  return false
296
239
  }
297
- const record = value as Readonly<Record<string, unknown>>
298
- return relation.data.fields.every(function checkField(declared) {
299
- const cell = record[declared.name]
300
- if (cell === undefined) {
301
- return false
302
- }
303
- return Result.isSuccess(
304
- Result.try({
305
- try: () => encodeBoundaryValue(declared.field, cell),
306
- catch: () => invalid("rowSchema")
307
- })
308
- )
309
- })
310
240
  })
311
241
  }
312
242
 
@@ -322,16 +252,14 @@ const encodeRows = Effect.fn("encodeRows")(function* <R extends AnyRelation>(
322
252
  rows: Iterable<Fact<R>>
323
253
  ) {
324
254
  const runtime = yield* runtimeHandle()
325
- const tables = schemaTables(shape.schema)
326
- const relationId = tables.relationIds.get(shape.relation.name)
327
- if (relationId === undefined || shape.schema.relations[shape.relation.name] !== shape.relation) {
328
- return yield* Effect.fail(invalid("encodeRows"))
329
- }
255
+ const { relation, relationId, spec } = yield* Effect.try({
256
+ try: () => checkedRowShape("encodeRows", shape),
257
+ catch: (cause) => argumentError("encodeRows", cause)
258
+ })
330
259
  const flat = yield* Effect.try({
331
- try: () => flatRowsOf(shape.relation.data, rows as Iterable<object>),
332
- catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeRows"))
260
+ try: () => flatRowsOf(relation, rows as Iterable<object>),
261
+ catch: (cause) => argumentError("encodeRows", cause)
333
262
  })
334
- const spec = lower(shape.schema)
335
263
  return yield* nativeOperationWith(
336
264
  "encodeRows",
337
265
  (callback) => dbNative.runtimeEncodeRows(runtime, spec, relationId, flat.rows, flat.cells, callback),
@@ -342,20 +270,18 @@ const encodeRows = Effect.fn("encodeRows")(function* <R extends AnyRelation>(
342
270
 
343
271
  const decodeRows = Effect.fn("decodeRows")(function* <R extends AnyRelation>(shape: RowShape<R>, input: Uint8Array) {
344
272
  const runtime = yield* runtimeHandle()
345
- const tables = schemaTables(shape.schema)
346
- const relationId = tables.relationIds.get(shape.relation.name)
347
- if (relationId === undefined || shape.schema.relations[shape.relation.name] !== shape.relation) {
348
- return yield* Effect.fail(invalid("decodeRows"))
349
- }
273
+ const { relation, relationId, spec } = yield* Effect.try({
274
+ try: () => checkedRowShape("decodeRows", shape),
275
+ catch: (cause) => argumentError("decodeRows", cause)
276
+ })
350
277
  if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer)) {
351
278
  return yield* Effect.fail(invalid("decodeRows"))
352
279
  }
353
- const spec = lower(shape.schema)
354
280
  return yield* nativeOperationWith(
355
281
  "decodeRows",
356
282
  (callback) => dbNative.runtimeDecodeRows(runtime, spec, relationId, input, callback),
357
283
  dbNative.runtimeRowsTake,
358
- (rows) => Object.freeze(rows.map((row) => factOfCells(shape.relation, row as readonly CellValue[])))
284
+ (rows) => Object.freeze(rows.map((row) => factOfCells(relation, row as readonly CellValue[])))
359
285
  )
360
286
  })
361
287
 
package/src/compile.ts CHANGED
@@ -1,22 +1,24 @@
1
1
  import { Effect } from "effect"
2
- import { isClosedMember } from "#closed.ts"
2
+ import { isClosedMember, membersAgree } from "#closed.ts"
3
3
  import { dbNative } from "#db-native.ts"
4
4
  import { SdkInvariantError } from "#errors.ts"
5
+ import { isImmutable, snapshotData } from "#immutable.ts"
5
6
  import type { SchemaClasses } from "#law.ts"
6
7
  import { lower } from "#lower.ts"
7
8
  import type { SealedDescriptor } from "#native.ts"
9
+ import type { AnyRelation } from "#relation.ts"
8
10
  import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
11
+ import { argumentError } from "#runtime-errors.ts"
9
12
  import type { AnySchema, Schema as SchemaDeclaration, SchemaRelations } from "#schema.ts"
10
- import type { Statement } from "#statements.ts"
13
+ import { schemaDescriptor } from "#schema.ts"
14
+ import { type KeyStatement, type Statement, statementDescriptor } from "#statements.ts"
11
15
 
12
16
  /**
13
17
  * `SchemaId` — the engine's canonical schema fingerprint as lowercase hex.
14
- * Independent of database identity; a branded string so arbitrary text does
15
- * not typecheck where a schema identity is required.
18
+ * Independent of database identity; ordinary structural text. Native
19
+ * boundaries verify the fingerprint wherever it grants admission.
16
20
  */
17
- declare const schemaIdBrand: unique symbol
18
-
19
- type SchemaId = string & { readonly [schemaIdBrand]: "bumbledb.SchemaId" }
21
+ type SchemaId = string
20
22
 
21
23
  /**
22
24
  * `CompiledSchema<S>` — bounded detached immutable descriptor data plus the
@@ -36,44 +38,35 @@ interface CompiledSchema<S extends AnySchema> {
36
38
  * ids (declaration order = ids), materialized statement ids (closed
37
39
  * relations' auto-handle keys FIRST in relation-declaration order, then
38
40
  * declared statements in declaration order with `mirrors` occupying two
39
- * consecutive slots, source-first — the theory's `StatementId` law), and
40
- * each relation's primary key (a closed relation's synthetic `(id)`,
41
- * otherwise its FIRST declared `key` statement).
41
+ * consecutive slots, source-first — the theory's `StatementId` law).
42
+ * Every declared statement retains its own native identity; no key is
43
+ * privileged by its position in the declaration.
42
44
  */
43
- interface PrimaryKey {
44
- readonly statementId: number
45
- readonly projection: readonly string[]
46
- }
47
-
48
45
  interface SchemaTables {
49
46
  readonly relationIds: ReadonlyMap<string, number>
50
- readonly primaryKeys: ReadonlyMap<string, PrimaryKey>
47
+ readonly statementIds: ReadonlyMap<Statement, number>
51
48
  }
52
49
 
53
50
  function declaredWidth(statement: Statement): number {
54
- return statement.data.kind === "mirrors" ? 2 : 1
51
+ return statement.kind === "mirrors" ? 2 : 1
55
52
  }
56
53
 
57
54
  function tablesOf(theory: AnySchema): SchemaTables {
58
55
  const relationIds = new Map<string, number>()
59
- const primaryKeys = new Map<string, PrimaryKey>()
56
+ const statementIds = new Map<Statement, number>()
60
57
  let autoKeys = 0
61
58
  Object.entries(theory.relations).forEach(function assignRelation([name, member], ordinal) {
62
59
  relationIds.set(name, ordinal)
63
60
  if (isClosedMember(member)) {
64
- primaryKeys.set(name, Object.freeze({ statementId: autoKeys, projection: Object.freeze(["id"]) }))
65
61
  autoKeys += 1
66
62
  }
67
63
  })
68
64
  let offset = autoKeys
69
65
  for (const statement of theory.statements) {
70
- const data = statement.data
71
- if (data.kind === "key" && !primaryKeys.has(data.owner.name)) {
72
- primaryKeys.set(data.owner.name, Object.freeze({ statementId: offset, projection: data.projection }))
73
- }
66
+ statementIds.set(statement, offset)
74
67
  offset += declaredWidth(statement)
75
68
  }
76
- return Object.freeze({ relationIds, primaryKeys })
69
+ return Object.freeze({ relationIds, statementIds })
77
70
  }
78
71
 
79
72
  const compiledCache = new WeakMap<AnySchema, SchemaTables>()
@@ -85,15 +78,37 @@ function schemaTables(theory: AnySchema): SchemaTables {
85
78
  return cached
86
79
  }
87
80
  const built = tablesOf(theory)
88
- compiledCache.set(theory, built)
81
+ if (isImmutable(theory)) compiledCache.set(theory, built)
89
82
  return built
90
83
  }
91
84
 
85
+ /** Resolve a logical key declaration, never an opaque constructor token. */
86
+ function declaredKey<R extends AnyRelation>(
87
+ theory: AnySchema,
88
+ input: KeyStatement<R>
89
+ ): { readonly key: KeyStatement<R>; readonly statementId: number } | undefined
90
+ function declaredKey(
91
+ theory: AnySchema,
92
+ input: KeyStatement
93
+ ): { readonly key: KeyStatement; readonly statementId: number } | undefined {
94
+ const key = statementDescriptor(input)
95
+ for (const [candidate, statementId] of schemaTables(theory).statementIds) {
96
+ if (
97
+ candidate.kind === "key" &&
98
+ membersAgree(candidate.owner, key.owner) &&
99
+ candidate.projection.length === key.projection.length &&
100
+ candidate.projection.every((field) => key.projection.includes(field))
101
+ )
102
+ return { key: candidate, statementId }
103
+ }
104
+ return undefined
105
+ }
106
+
92
107
  function admitSchemaId(fingerprint: string): SchemaId {
93
- if (typeof fingerprint !== "string" || fingerprint.length === 0) {
108
+ if (typeof fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(fingerprint)) {
94
109
  throw new SdkInvariantError({ message: "Schema.compile: the engine returned no canonical fingerprint" })
95
110
  }
96
- return fingerprint as SchemaId
111
+ return fingerprint
97
112
  }
98
113
 
99
114
  /**
@@ -105,18 +120,27 @@ function admitSchemaId(fingerprint: string): SchemaId {
105
120
  * native finalizer is created.
106
121
  */
107
122
  const compile = Effect.fn("Schema.compile")(function* <S extends AnySchema>(schema: S) {
123
+ const owned = yield* Effect.try({
124
+ try: () => schemaDescriptor(schema),
125
+ catch: (cause) => argumentError("Schema.compile", cause)
126
+ })
127
+ const spec = yield* Effect.try({ try: () => lower(owned), catch: (cause) => argumentError("Schema.compile", cause) })
108
128
  const handle = yield* runtimeHandle()
109
- const spec = lower(schema)
110
129
  const descriptor = yield* nativeOperationWith(
111
130
  "Schema.compile",
112
131
  (callback) => dbNative.runtimeSchemaCompile(handle, spec, callback),
113
132
  dbNative.runtimeSchemaTake,
114
133
  (value) => value
115
134
  )
135
+ const ownedDescriptor = snapshotData(descriptor)
116
136
  const compiled: CompiledSchema<S> = Object.freeze({
117
- schema,
137
+ get schema() {
138
+ return snapshotData(owned)
139
+ },
118
140
  schemaId: admitSchemaId(descriptor.fingerprint),
119
- descriptor
141
+ get descriptor() {
142
+ return snapshotData(ownedDescriptor)
143
+ }
120
144
  })
121
145
  return compiled
122
146
  })
@@ -143,5 +167,5 @@ type Schema<Rels extends SchemaRelations, Classes extends SchemaClasses = Schema
143
167
  */
144
168
  const Schema = Object.freeze({ compile })
145
169
 
146
- export type { CompiledSchema, PrimaryKey, SchemaId, SchemaTables }
147
- export { Schema, schemaTables }
170
+ export type { CompiledSchema, SchemaId, SchemaTables }
171
+ export { declaredKey, Schema, schemaTables }
package/src/db-native.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  * and joins any of them. Close verbs report the real drain outcome through
7
7
  * `CloseWire`.
8
8
  */
9
+
10
+ import type { ChangeCounts } from "#changes.ts"
9
11
  import type { DbHandle, ParsedQuery, QueryParam, SealedDescriptor, Violation } from "#native.ts"
10
12
  import { native } from "#native.ts"
11
13
  import type { CellValue } from "#rows.ts"
@@ -30,6 +32,9 @@ export interface DraftHandle {
30
32
  export interface ChangesHandle {
31
33
  readonly __changes: unique symbol
32
34
  }
35
+ export interface ChangesCursorHandle {
36
+ readonly __changesCursor: unique symbol
37
+ }
33
38
 
34
39
  /** The core-local witness: catalog/store identity plus generation, never a StateStamp. */
35
40
  export interface WitnessWire {
@@ -47,6 +52,16 @@ export type ApplyOutcomeWire =
47
52
  | { readonly tag: "invariant-rejected"; readonly violations: readonly Violation[] }
48
53
  | { readonly tag: "moved"; readonly witnessed: WitnessWire; readonly current: WitnessWire }
49
54
 
55
+ export type JudgeOutcomeWire =
56
+ | { readonly tag: "admitted"; readonly base: WitnessWire; readonly changes: ChangeCounts }
57
+ | {
58
+ readonly tag: "invariant-rejected"
59
+ readonly base: WitnessWire
60
+ readonly changes: ChangeCounts
61
+ readonly violations: readonly Violation[]
62
+ }
63
+ | { readonly tag: "moved"; readonly witnessed: WitnessWire; readonly current: WitnessWire }
64
+
50
65
  /** Bounded database diagnostics: measurements, never retained row payloads. */
51
66
  export interface DbInspectionWire {
52
67
  readonly generation: bigint
@@ -62,6 +77,14 @@ export interface SnapshotWire {
62
77
  export interface ChangesWire {
63
78
  readonly changes: ChangesHandle
64
79
  readonly fingerprint: string
80
+ readonly counts: ChangeCounts
81
+ readonly byteLength: bigint
82
+ }
83
+
84
+ export interface ChangeRecordWire {
85
+ readonly relation: number
86
+ readonly kind: "add" | "remove"
87
+ readonly values: readonly CellValue[]
65
88
  }
66
89
 
67
90
  export interface MutationReportWire {
@@ -158,10 +181,29 @@ interface DbBridge {
158
181
  runtimeChangesTake(operation: OperationHandle): ChangesWire
159
182
  runtimeDraftClose(draft: DraftHandle, callback: (report: CloseWire) => void): void
160
183
  runtimeChangesClose(changes: ChangesHandle, callback: (report: CloseWire) => void): void
184
+ /** Owned immutable bytes; parsing checks canonical order and the supplied schema. */
185
+ runtimeChangesBytes(changes: ChangesHandle, callback: () => void): OperationHandle
186
+ runtimeChangesParse(
187
+ runtime: RuntimeHandle,
188
+ spec: SchemaSpec,
189
+ bytes: Uint8Array,
190
+ callback: () => void
191
+ ): OperationHandle
192
+ /** One command's add-wins composition, not sequential application. */
193
+ runtimeChangesCompose(left: ChangesHandle, right: ChangesHandle, callback: () => void): OperationHandle
194
+ /** Independent bounded traversal; never spends the immutable source. */
195
+ runtimeChangesCursor(changes: ChangesHandle, callback: () => void): OperationHandle
196
+ runtimeChangesCursorTake(operation: OperationHandle): ChangesCursorHandle
197
+ runtimeChangesCursorNext(cursor: ChangesCursorHandle, callback: () => void): OperationHandle
198
+ runtimeChangePageTake(operation: OperationHandle): readonly ChangeRecordWire[] | null
199
+ runtimeChangesCursorClose(cursor: ChangesCursorHandle, callback: (report: CloseWire) => void): void
161
200
 
162
201
  /** One immutable final-state admission/commit under the managed owner. */
163
202
  runtimeDbApply(db: DbHandle, changes: ChangesHandle, expected: ExpectedWire, callback: () => void): OperationHandle
164
203
  runtimeApplyTake(operation: OperationHandle): ApplyOutcomeWire
204
+ /** Same candidate admission as apply, aborted on its worker before returning. */
205
+ runtimeDbJudge(db: DbHandle, changes: ChangesHandle, expected: ExpectedWire, callback: () => void): OperationHandle
206
+ runtimeJudgeTake(operation: OperationHandle): JudgeOutcomeWire
165
207
 
166
208
  runtimeDbInspect(db: DbHandle, callback: () => void): OperationHandle
167
209
  runtimeDbClearCache(db: DbHandle, callback: () => void): OperationHandle