@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/codec.ts CHANGED
@@ -1,19 +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
- import type { ExecutionPolicy } from "#runtime.ts"
11
- import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
12
- import { DbError } from "#runtime-errors.ts"
11
+ import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
12
+ import { argumentError, DbError } from "#runtime-errors.ts"
13
13
  import type { AnySchema } from "#schema.ts"
14
+ import { schemaDescriptor } from "#schema.ts"
14
15
  import type { Rel } from "#shape.ts"
15
16
  import { f64BitsHex } from "#spec.ts"
16
- import { Uuid } from "#uuid.ts"
17
+ import { fieldValue, recordValue } from "#values.ts"
17
18
 
18
19
  /**
19
20
  * Boundary row codecs, derived from the core relation descriptors — never a
@@ -22,7 +23,7 @@ import { Uuid } from "#uuid.ts"
22
23
  * Two layers:
23
24
  *
24
25
  * 1. The CANONICAL native row codec: `encodeRows`/`decodeRows` run the
25
- * same charged native implementation the engine, log and migrations
26
+ * same native implementation the engine, log and migrations
26
27
  * share (owned bytes in, owned typed rows out; untrusted input cannot
27
28
  * inject native capabilities). Effect-only, `NativeRuntime` required.
28
29
  * 2. The schema-tagged JSON VALUE form for HTTP/export boundaries
@@ -44,14 +45,27 @@ interface RowShape<R extends AnyRelation> {
44
45
  }
45
46
 
46
47
  function rowShape<S extends AnySchema, R extends Rel<S>>(schema: S, relation: R): RowShape<R> {
47
- 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) }
48
62
  }
49
63
 
50
64
  function invalid(operation: string): DbError {
51
65
  return new DbError({ operation, reason: { _tag: "InvalidArgument" } })
52
66
  }
53
67
 
54
- const DECIMAL = /^-?(?:0|[1-9][0-9]*)$/
68
+ const DECIMAL = /^(?:0|-?[1-9][0-9]*)$/
55
69
  const HEX16 = /^[0-9a-f]{16}$/
56
70
  const LOWER_HEX = /^(?:[0-9a-f]{2})*$/
57
71
 
@@ -89,146 +103,71 @@ function f64OfHex(text: unknown): number | undefined {
89
103
  return value
90
104
  }
91
105
 
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
- }
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)
101
109
  switch (field.kind) {
102
- case "bool": {
103
- if (typeof value !== "boolean") {
104
- throw invalid("encodeBoundaryRows")
105
- }
106
- return value
107
- }
110
+ case "bool":
111
+ case "str":
112
+ case "uuid":
113
+ return fieldValue(context, field, input)
108
114
  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
- }
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))
139
121
  case "interval": {
140
122
  if (field.element === "f64") {
141
- if (!isFloatIntervalValue(value)) {
142
- throw invalid("encodeBoundaryRows")
143
- }
123
+ const value = fieldValue(context, { ...field, element: "f64" }, input)
144
124
  return { start: { $f64: f64BitsHex(value.start) }, end: { $f64: f64BitsHex(value.end) } }
145
125
  }
146
- if (!isIntervalValue(value)) {
147
- throw invalid("encodeBoundaryRows")
148
- }
126
+ const value = fieldValue(context, { ...field, element: field.element }, input)
149
127
  return { start: value.start.toString(10), end: value.end.toString(10) }
150
128
  }
151
129
  }
152
130
  }
153
131
 
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
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)
171
136
  }
172
137
 
173
138
  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)
139
+ const record = recordValue("binary64 image", value, ["$f64"])
140
+ return f64OfHex(record.$f64)
182
141
  }
183
142
 
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
- }
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
190
147
  switch (field.kind) {
191
- case "bool":
192
- return typeof value === "boolean" ? value : undefined
193
148
  case "u64":
194
149
  case "i64":
195
- return decodeInteger(field.kind, value)
150
+ decoded = decodeInteger(input)
151
+ break
196
152
  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
- }
153
+ decoded = decodeTaggedF64(input)
154
+ break
155
+ case "bytes":
156
+ decoded = typeof input === "string" && input.length === field.width * 2 ? bytesOfHex(input) : undefined
157
+ break
208
158
  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 })
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
227
165
  }
228
166
  }
167
+ return fieldValue(context, field, decoded)
229
168
  }
230
169
 
231
- /** Pure schema-tagged JSON encoding of owned rows (bounded per call). */
170
+ /** Pure schema-tagged JSON encoding of complete rows. */
232
171
  function encodeBoundaryRows<R extends AnyRelation>(
233
172
  relation: R,
234
173
  rows: Iterable<Fact<R>>
@@ -236,83 +175,73 @@ function encodeBoundaryRows<R extends AnyRelation>(
236
175
  return Result.try({
237
176
  try: () => {
238
177
  const out: Array<Readonly<Record<string, unknown>>> = []
178
+ const fields = relationFields(relation)
179
+ const names = fields.map((declared) => declared.name)
239
180
  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]
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
+ )
245
190
  )
246
- }
247
- out.push(Object.freeze(record))
191
+ )
248
192
  }
249
193
  return Object.freeze(out)
250
194
  },
251
- catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeBoundaryRows"))
195
+ catch: (cause) => argumentError("encodeBoundaryRows", cause)
252
196
  })
253
197
  }
254
198
 
255
- /** Pure strict decoding of schema-tagged JSON rows; any refusal is typed. */
199
+ /** Pure strict decoding; malformed records, getters and values fail as data. */
256
200
  function decodeBoundaryRows<R extends AnyRelation>(
257
201
  relation: R,
258
202
  input: unknown
259
203
  ): 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))
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
+ })
285
225
  }
286
226
 
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
- */
227
+ /** An Effect Schema derived from the same complete-row value interpreter. */
293
228
  function rowSchema<R extends AnyRelation>(relation: R) {
229
+ const fields = relationFields(relation)
230
+ const names = fields.map((declared) => declared.name)
294
231
  return EffectSchema.declare((value: unknown): value is Fact<R> => {
295
- 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 {
296
238
  return false
297
239
  }
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
240
  })
312
241
  }
313
242
 
314
243
  /**
315
- * The charged CANONICAL native row codec (chapter 35 roster): owned bytes
244
+ * The canonical native row codec (chapter 35 roster): owned bytes
316
245
  * out, owned typed rows back in — the same implementation log sealing and
317
246
  * migrations use. Binding parameters is ingestion: input must stay stable
318
247
  * through execution, and no native work starts before the checked owned
@@ -320,58 +249,39 @@ function rowSchema<R extends AnyRelation>(relation: R) {
320
249
  */
321
250
  const encodeRows = Effect.fn("encodeRows")(function* <R extends AnyRelation>(
322
251
  shape: RowShape<R>,
323
- rows: Iterable<Fact<R>>,
324
- work: ExecutionPolicy
252
+ rows: Iterable<Fact<R>>
325
253
  ) {
326
254
  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
- }
255
+ const { relation, relationId, spec } = yield* Effect.try({
256
+ try: () => checkedRowShape("encodeRows", shape),
257
+ catch: (cause) => argumentError("encodeRows", cause)
258
+ })
332
259
  const flat = yield* Effect.try({
333
- try: () => flatRowsOf(shape.relation.data, rows as Iterable<object>),
334
- catch: (cause) => (cause instanceof DbError ? cause : invalid("encodeRows"))
260
+ try: () => flatRowsOf(relation, rows as Iterable<object>),
261
+ catch: (cause) => argumentError("encodeRows", cause)
335
262
  })
336
- const spec = lower(shape.schema)
337
263
  return yield* nativeOperationWith(
338
264
  "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
- ),
265
+ (callback) => dbNative.runtimeEncodeRows(runtime, spec, relationId, flat.rows, flat.cells, callback),
349
266
  dbNative.runtimeBytesTake,
350
267
  (bytes) => bytes
351
268
  )
352
269
  })
353
270
 
354
- const decodeRows = Effect.fn("decodeRows")(function* <R extends AnyRelation>(
355
- shape: RowShape<R>,
356
- input: Uint8Array,
357
- work: ExecutionPolicy
358
- ) {
271
+ const decodeRows = Effect.fn("decodeRows")(function* <R extends AnyRelation>(shape: RowShape<R>, input: Uint8Array) {
359
272
  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
- }
273
+ const { relation, relationId, spec } = yield* Effect.try({
274
+ try: () => checkedRowShape("decodeRows", shape),
275
+ catch: (cause) => argumentError("decodeRows", cause)
276
+ })
365
277
  if (!(input instanceof Uint8Array) || !(input.buffer instanceof ArrayBuffer)) {
366
278
  return yield* Effect.fail(invalid("decodeRows"))
367
279
  }
368
- const spec = lower(shape.schema)
369
280
  return yield* nativeOperationWith(
370
281
  "decodeRows",
371
- (callback) =>
372
- dbNative.runtimeDecodeRows(runtime, policyWire(work, "decodeRows"), spec, relationId, input, callback),
282
+ (callback) => dbNative.runtimeDecodeRows(runtime, spec, relationId, input, callback),
373
283
  dbNative.runtimeRowsTake,
374
- (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[])))
375
285
  )
376
286
  })
377
287
 
package/src/compile.ts CHANGED
@@ -1,23 +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"
8
- import type { ExecutionPolicy } from "#runtime.ts"
9
- import { nativeOperationWith, policyWire, runtimeHandle } from "#runtime.ts"
9
+ import type { AnyRelation } from "#relation.ts"
10
+ import { nativeOperationWith, runtimeHandle } from "#runtime.ts"
11
+ import { argumentError } from "#runtime-errors.ts"
10
12
  import type { AnySchema, Schema as SchemaDeclaration, SchemaRelations } from "#schema.ts"
11
- import type { Statement } from "#statements.ts"
13
+ import { schemaDescriptor } from "#schema.ts"
14
+ import { type KeyStatement, type Statement, statementDescriptor } from "#statements.ts"
12
15
 
13
16
  /**
14
17
  * `SchemaId` — the engine's canonical schema fingerprint as lowercase hex.
15
- * Independent of database identity; a branded string so arbitrary text does
16
- * 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.
17
20
  */
18
- declare const schemaIdBrand: unique symbol
19
-
20
- type SchemaId = string & { readonly [schemaIdBrand]: "bumbledb.SchemaId" }
21
+ type SchemaId = string
21
22
 
22
23
  /**
23
24
  * `CompiledSchema<S>` — bounded detached immutable descriptor data plus the
@@ -37,44 +38,35 @@ interface CompiledSchema<S extends AnySchema> {
37
38
  * ids (declaration order = ids), materialized statement ids (closed
38
39
  * relations' auto-handle keys FIRST in relation-declaration order, then
39
40
  * declared statements in declaration order with `mirrors` occupying two
40
- * consecutive slots, source-first — the theory's `StatementId` law), and
41
- * each relation's primary key (a closed relation's synthetic `(id)`,
42
- * 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.
43
44
  */
44
- interface PrimaryKey {
45
- readonly statementId: number
46
- readonly projection: readonly string[]
47
- }
48
-
49
45
  interface SchemaTables {
50
46
  readonly relationIds: ReadonlyMap<string, number>
51
- readonly primaryKeys: ReadonlyMap<string, PrimaryKey>
47
+ readonly statementIds: ReadonlyMap<Statement, number>
52
48
  }
53
49
 
54
50
  function declaredWidth(statement: Statement): number {
55
- return statement.data.kind === "mirrors" ? 2 : 1
51
+ return statement.kind === "mirrors" ? 2 : 1
56
52
  }
57
53
 
58
54
  function tablesOf(theory: AnySchema): SchemaTables {
59
55
  const relationIds = new Map<string, number>()
60
- const primaryKeys = new Map<string, PrimaryKey>()
56
+ const statementIds = new Map<Statement, number>()
61
57
  let autoKeys = 0
62
58
  Object.entries(theory.relations).forEach(function assignRelation([name, member], ordinal) {
63
59
  relationIds.set(name, ordinal)
64
60
  if (isClosedMember(member)) {
65
- primaryKeys.set(name, Object.freeze({ statementId: autoKeys, projection: Object.freeze(["id"]) }))
66
61
  autoKeys += 1
67
62
  }
68
63
  })
69
64
  let offset = autoKeys
70
65
  for (const statement of theory.statements) {
71
- const data = statement.data
72
- if (data.kind === "key" && !primaryKeys.has(data.owner.name)) {
73
- primaryKeys.set(data.owner.name, Object.freeze({ statementId: offset, projection: data.projection }))
74
- }
66
+ statementIds.set(statement, offset)
75
67
  offset += declaredWidth(statement)
76
68
  }
77
- return Object.freeze({ relationIds, primaryKeys })
69
+ return Object.freeze({ relationIds, statementIds })
78
70
  }
79
71
 
80
72
  const compiledCache = new WeakMap<AnySchema, SchemaTables>()
@@ -86,38 +78,69 @@ function schemaTables(theory: AnySchema): SchemaTables {
86
78
  return cached
87
79
  }
88
80
  const built = tablesOf(theory)
89
- compiledCache.set(theory, built)
81
+ if (isImmutable(theory)) compiledCache.set(theory, built)
90
82
  return built
91
83
  }
92
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
+
93
107
  function admitSchemaId(fingerprint: string): SchemaId {
94
- if (typeof fingerprint !== "string" || fingerprint.length === 0) {
108
+ if (typeof fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(fingerprint)) {
95
109
  throw new SdkInvariantError({ message: "Schema.compile: the engine returned no canonical fingerprint" })
96
110
  }
97
- return fingerprint as SchemaId
111
+ return fingerprint
98
112
  }
99
113
 
100
114
  /**
101
- * Charged, effectful native schema admission/compilation: a
115
+ * Effectful native schema admission/compilation: a
102
116
  * schema declaration is pure metadata and never a claim its theory has been
103
- * admitted. Runs on the one bounded executor under `work`, requires the
117
+ * admitted. Runs on the shared executor, requires the
104
118
  * acquired {@link NativeRuntime}, and yields detached immutable descriptor
105
119
  * data plus the canonical schema identity. No database is opened and no
106
120
  * native finalizer is created.
107
121
  */
108
- const compile = Effect.fn("Schema.compile")(function* <S extends AnySchema>(schema: S, work: ExecutionPolicy) {
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) })
109
128
  const handle = yield* runtimeHandle()
110
- const spec = lower(schema)
111
129
  const descriptor = yield* nativeOperationWith(
112
130
  "Schema.compile",
113
- (callback) => dbNative.runtimeSchemaCompile(handle, policyWire(work, "Schema.compile"), spec, callback),
131
+ (callback) => dbNative.runtimeSchemaCompile(handle, spec, callback),
114
132
  dbNative.runtimeSchemaTake,
115
133
  (value) => value
116
134
  )
135
+ const ownedDescriptor = snapshotData(descriptor)
117
136
  const compiled: CompiledSchema<S> = Object.freeze({
118
- schema,
137
+ get schema() {
138
+ return snapshotData(owned)
139
+ },
119
140
  schemaId: admitSchemaId(descriptor.fingerprint),
120
- descriptor
141
+ get descriptor() {
142
+ return snapshotData(ownedDescriptor)
143
+ }
121
144
  })
122
145
  return compiled
123
146
  })
@@ -144,5 +167,5 @@ type Schema<Rels extends SchemaRelations, Classes extends SchemaClasses = Schema
144
167
  */
145
168
  const Schema = Object.freeze({ compile })
146
169
 
147
- export type { CompiledSchema, PrimaryKey, SchemaId, SchemaTables }
148
- export { Schema, schemaTables }
170
+ export type { CompiledSchema, SchemaId, SchemaTables }
171
+ export { declaredKey, Schema, schemaTables }