@minnowdb/core 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +23 -32
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +41 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -10
  43. package/dist/engine/index.js +12 -9
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +504 -126
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +262 -27
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -85
  115. package/dist/storage/snapshot.js +33 -265
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
package/README.md CHANGED
@@ -1,42 +1,33 @@
1
1
  # @minnowdb/core
2
2
 
3
- Columnar SQL engine for the browser: real SQL over immutable snapshots, durable on IndexedDB or
4
- OPFS, with
5
- no WebAssembly to download. It runs in the thread that constructs it (`MinnowDatabase`), or in a
6
- worker through `MinnowDatabaseClient`, which mirrors the everyday query, write, migration,
7
- live-query, snapshot, and maintenance APIs.
8
-
9
- **[minnowdb.com](https://minnowdb.com)** — documentation, a live console, and benchmarks you
10
- run yourself.
3
+ A columnar SQL database for the browser. Run PostgreSQL-style SQL over durable IndexedDB or OPFS
4
+ storage with no server and no WebAssembly module.
11
5
 
12
6
  ```bash
13
7
  npm install @minnowdb/core
14
8
  ```
15
9
 
16
- - **SQL is the foundation** — parser, planner, optimizer, and a batch-based executor written here,
17
- with no SQLite or DuckDB underneath. The supported surface ships as a checked-in feature matrix
18
- (`@minnowdb/core/sql-feature-matrix.json`) that the engine is tested against.
19
- - **One schema, in TypeScript** `table()`, `column`, and `schema()` declare tables once and
20
- `migrate()` evolves them without rewriting stored data.
21
- - **Full-text search** — `MATCH` / BM25 on any column, with no index DDL to write.
22
- - **Live queries** `liveQueries()` hands back a set whose subscribers get a fresh result after
23
- any commit that could have changed it, across tabs.
24
- - **Durable browser storage** — compressed columnar blocks on IndexedDB or OPFS, atomic
25
- multi-tab commits,
26
- snapshot reads, compaction and GC.
27
- - **Memory-aware queries** — execution works in batches under a budget you set, and sorts and
28
- grouped results can spill to storage. The budget is not a hard heap limit yet.
29
- - **Subpath exports** `/storage`, `/storage/toolkit`, `/client`, `/worker`,
30
- `/worker-protocol`, `/transactions`, `/plan`, `/block-format`, `/testing`.
31
-
32
- Guides, the API reference, and the SQL feature matrix live in the
33
- [docs](https://minnowdb.com/docs/), which are the single source of truth. Building with an agent?
34
- [minnowdb.com/agent-rules.md](https://minnowdb.com/agent-rules.md) is a short rules file to drop
35
- into an `AGENTS.md`, and [llms.txt](https://minnowdb.com/llms.txt) indexes the documentation in
36
- markdown.
37
-
38
- Every `@minnowdb` package shares a major version and moves independently inside it, so install
39
- them on the same major. See [Versioning](https://minnowdb.com/docs/reference/versioning/).
10
+ - Direct SQL through `MinnowDatabase.query()` and `execute()`.
11
+ - Joins, CTEs, window functions, grouping sets, upserts, `RETURNING`, triggers, exact decimals,
12
+ JSON/JSONB, arrays, enums, sequences, and savepoints.
13
+ - Compressed column storage, secondary indexes, full-text search, and snapshot reads.
14
+ - Atomic writes across tabs through IndexedDB or OPFS, strict durability by default, and explicit
15
+ origin-eviction persistence policy.
16
+ - A ready-made worker client with the same everyday database API.
17
+ - TypeScript schema declarations and metadata-only migrations, including SQL domains and
18
+ composite primary/foreign keys.
19
+ - Batch writes, pull-driven query cursors, live queries, snapshots, compaction, and configurable
20
+ query memory.
21
+
22
+ Use [the PostgreSQL compatibility page](https://minnowdb.com/docs/sql/feature-matrix/) for the
23
+ exact SQL surface. Use [the documentation](https://minnowdb.com/docs/) for installation, storage,
24
+ workers, transactions, and API details.
25
+
26
+ The optional [Kysely dialect](https://minnowdb.com/docs/adapters/kysely/) connects Kysely's
27
+ PostgreSQL compiler to the engine and derives its `DB` types from the same schema declaration.
28
+
29
+ Every `@minnowdb` package in one application must use the same major version. See
30
+ [Versioning](https://minnowdb.com/docs/reference/versioning/).
40
31
 
41
32
  ## License
42
33
 
@@ -1,6 +1,6 @@
1
1
  import type { BlockDescription, BlockMetadata, ColumnInput, Compression, DecodedBlock, DecodedPhysicalBlock, PhysicalColumnPayload } from "./types.js";
2
2
  /**
3
- * Version 1 layout (all integers little-endian):
3
+ * Version 2 layout (all integers little-endian):
4
4
  *
5
5
  * 0 magic "BRDB"
6
6
  * 4 envelope checksum: crc32 over bytes [8, headerLength + metadataLength)
@@ -12,35 +12,54 @@ import type { BlockDescription, BlockMetadata, ColumnInput, Compression, Decoded
12
12
  * 24 metadata length (u32)
13
13
  * 28 encoded length (u32)
14
14
  * 32 stored length (u32)
15
- * 36 payload checksum: crc32 over the uncompressed encoded payload
15
+ * 36 logical checksum: crc32 over the uncompressed encoded payload
16
+ * 40 stored checksum: crc32 over the stored payload, before decompression
16
17
  *
17
- * The envelope checksum authenticates every header field and the metadata JSON, so header-only
18
+ * The envelope checksum integrity-checks every header field and the metadata JSON, so header-only
18
19
  * reads (zone-map pruning, block inventories) can trust derived statistics without paying the
19
20
  * decompress-and-revalidate cost of the payload.
20
21
  */
21
22
  /**
22
- * The block envelope version. Every persisted block carries it, and `readBlock` refuses any
23
- * value it does not recognize -- so this number is a compatibility contract with every database
24
- * already sitting in a user's browser, not an internal detail.
23
+ * The first frozen block envelope version. Every persisted block carries it, and readers dispatch
24
+ * by this value, so the number is a compatibility contract rather than an internal detail.
25
25
  *
26
- * Changing it means older blocks stop being readable unless a read path for them is kept. Before
27
- * changing it, run `npm run fixture:format` on the *current* build to freeze what this version
28
- * writes; format-compatibility.test.ts then holds the new build to still reading it.
26
+ * Never change v2 decoding in place. A future writer gets a new number and keeps the v2 read path;
27
+ * format-vectors.test.ts permanently freezes Minnow-owned v2 bytes.
29
28
  */
30
- export declare const BLOCK_FORMAT_VERSION = 1;
29
+ export declare const BLOCK_FORMAT_VERSION = 2;
30
+ export declare const BLOCK_HEADER_LENGTH = 44;
31
+ export declare const MAX_BLOCK_METADATA_BYTE_LENGTH = 1024;
32
+ /** Absolute persisted value ceiling: envelope + metadata + maximum stored payload. */
33
+ export declare const MAX_STORED_BLOCK_BYTE_LENGTH: number;
34
+ /** A codec expanded an otherwise valid physical column beyond the stored block hard limit. */
35
+ export declare class StoredBlockPayloadTooLargeError extends RangeError {
36
+ readonly byteLength: number;
37
+ constructor(byteLength: number);
38
+ }
39
+ /** Validates a codec result length without allocating the payload. */
40
+ export declare function assertStoredBlockPayloadByteLength(byteLength: number): void;
31
41
  /**
32
42
  * Returns a conservative bound for the complete persisted block value: the fixed header, the
33
43
  * exact UTF-8 JSON metadata, and the selected codec's maximum stored payload.
34
44
  */
35
45
  export declare function maximumPhysicalBlockByteLength(encodedByteLength: number, metadata: BlockMetadata, compression: Compression): number;
36
46
  export declare function encodeBlock(input: ColumnInput, compression?: Compression): Promise<Uint8Array>;
37
- /** Encodes physical column bytes after validating their complete version-zero layout. */
47
+ /** Encodes physical column bytes after validating their complete canonical v2 layout. */
38
48
  export declare function encodePhysicalBlock(input: PhysicalColumnPayload, compression?: Compression): Promise<Uint8Array>;
39
49
  export declare function decodeBlock(bytes: Uint8Array): Promise<DecodedBlock>;
40
50
  /**
41
51
  * Decompresses and checksum-verifies a block without materializing row values.
42
- * The returned physical byte array is owned by the result.
52
+ *
53
+ * For a raw block, `column.bytes` is a zero-copy view into `bytes`. Both views must be treated as
54
+ * immutable for the lifetime of the result. Gzip results own a new decompressed allocation.
43
55
  */
44
56
  export declare function decodePhysicalBlock(bytes: Uint8Array): Promise<DecodedPhysicalBlock>;
57
+ /**
58
+ * Verifies every stored byte without decompressing or copying the payload.
59
+ *
60
+ * Snapshot import and OPFS recovery use this stronger operation before accepting a block.
61
+ * Planning and pruning should continue to use `inspectBlock`, which deliberately reads only the
62
+ * fixed header and metadata envelope.
63
+ */
64
+ export declare function verifyStoredBlock(bytes: Uint8Array): BlockDescription;
45
65
  export declare function inspectBlock(bytes: Uint8Array): BlockDescription;
46
- //# sourceMappingURL=block.d.ts.map
@@ -1,10 +1,10 @@
1
1
  import { crc32 } from "./checksum.js";
2
- import { decodeColumn, encodeColumn } from "./column.js";
3
- import { getCodec, getCompressionMemoryBound } from "./codecs.js";
4
- import { MAX_PHYSICAL_COLUMN_BYTE_LENGTH, validatePhysicalColumn } from "./physical.js";
2
+ import { decodeValidatedColumn, encodeColumn } from "./column.js";
3
+ import { CompressionOutputLimitError, getCodec, getCompressionMemoryBound } from "./codecs.js";
4
+ import { assertBlockRowCount, MAX_PHYSICAL_COLUMN_BYTE_LENGTH, validatePhysicalColumn, } from "./physical.js";
5
5
  const MAGIC = Uint8Array.of(0x42, 0x52, 0x44, 0x42);
6
6
  /**
7
- * Version 1 layout (all integers little-endian):
7
+ * Version 2 layout (all integers little-endian):
8
8
  *
9
9
  * 0 magic "BRDB"
10
10
  * 4 envelope checksum: crc32 over bytes [8, headerLength + metadataLength)
@@ -16,53 +16,64 @@ const MAGIC = Uint8Array.of(0x42, 0x52, 0x44, 0x42);
16
16
  * 24 metadata length (u32)
17
17
  * 28 encoded length (u32)
18
18
  * 32 stored length (u32)
19
- * 36 payload checksum: crc32 over the uncompressed encoded payload
19
+ * 36 logical checksum: crc32 over the uncompressed encoded payload
20
+ * 40 stored checksum: crc32 over the stored payload, before decompression
20
21
  *
21
- * The envelope checksum authenticates every header field and the metadata JSON, so header-only
22
+ * The envelope checksum integrity-checks every header field and the metadata JSON, so header-only
22
23
  * reads (zone-map pruning, block inventories) can trust derived statistics without paying the
23
24
  * decompress-and-revalidate cost of the payload.
24
25
  */
25
26
  /**
26
- * The block envelope version. Every persisted block carries it, and `readBlock` refuses any
27
- * value it does not recognize -- so this number is a compatibility contract with every database
28
- * already sitting in a user's browser, not an internal detail.
27
+ * The first frozen block envelope version. Every persisted block carries it, and readers dispatch
28
+ * by this value, so the number is a compatibility contract rather than an internal detail.
29
29
  *
30
- * Changing it means older blocks stop being readable unless a read path for them is kept. Before
31
- * changing it, run `npm run fixture:format` on the *current* build to freeze what this version
32
- * writes; format-compatibility.test.ts then holds the new build to still reading it.
30
+ * Never change v2 decoding in place. A future writer gets a new number and keeps the v2 read path;
31
+ * format-vectors.test.ts permanently freezes Minnow-owned v2 bytes.
33
32
  */
34
- export const BLOCK_FORMAT_VERSION = 1;
33
+ export const BLOCK_FORMAT_VERSION = 2;
35
34
  const VERSION = BLOCK_FORMAT_VERSION;
36
- const HEADER_LENGTH = 40;
35
+ export const BLOCK_HEADER_LENGTH = 44;
36
+ const HEADER_LENGTH = BLOCK_HEADER_LENGTH;
37
37
  const ENVELOPE_CHECKSUM_START = 8;
38
38
  const MAX_BLOCK_LENGTH = MAX_PHYSICAL_COLUMN_BYTE_LENGTH;
39
+ export const MAX_BLOCK_METADATA_BYTE_LENGTH = 1_024;
40
+ const MAX_METADATA_LENGTH = MAX_BLOCK_METADATA_BYTE_LENGTH;
41
+ /** Absolute persisted value ceiling: envelope + metadata + maximum stored payload. */
42
+ export const MAX_STORED_BLOCK_BYTE_LENGTH = BLOCK_HEADER_LENGTH + MAX_METADATA_LENGTH + MAX_PHYSICAL_COLUMN_BYTE_LENGTH;
39
43
  const textEncoder = new TextEncoder();
40
44
  const textDecoder = new TextDecoder("utf-8", { fatal: true });
41
45
  const typeIds = { boolean: 1, number: 2, string: 3, datetime: 4 };
42
46
  const typesById = new Map(Object.entries(typeIds).map(([type, id]) => [id, type]));
43
- // Codec 1 was run-length encoding, removed because it was both slower and larger than raw on
44
- // every column shape measured. The ids of the survivors do not move: renumbering would make an
45
- // old block decode as the wrong codec instead of failing.
47
+ // Codec 1 was a pre-freeze run-length prototype, removed because it was both slower and larger
48
+ // than raw on every measured column shape. Keep the id permanently reserved so it can never be
49
+ // confused with a different codec in diagnostic or development data.
46
50
  const codecIds = { raw: 0, gzip: 2 };
47
51
  const codecsById = new Map(Object.entries(codecIds).map(([codec, id]) => [id, codec]));
52
+ /** A codec expanded an otherwise valid physical column beyond the stored block hard limit. */
53
+ export class StoredBlockPayloadTooLargeError extends RangeError {
54
+ byteLength;
55
+ constructor(byteLength) {
56
+ super("Stored block payload exceeds maximum byte length");
57
+ this.byteLength = byteLength;
58
+ this.name = "StoredBlockPayloadTooLargeError";
59
+ }
60
+ }
61
+ /** Validates a codec result length without allocating the payload. */
62
+ export function assertStoredBlockPayloadByteLength(byteLength) {
63
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
64
+ throw new RangeError("Invalid stored payload length");
65
+ }
66
+ if (byteLength > MAX_BLOCK_LENGTH)
67
+ throw new StoredBlockPayloadTooLargeError(byteLength);
68
+ }
48
69
  /**
49
70
  * Returns a conservative bound for the complete persisted block value: the fixed header, the
50
71
  * exact UTF-8 JSON metadata, and the selected codec's maximum stored payload.
51
72
  */
52
73
  export function maximumPhysicalBlockByteLength(encodedByteLength, metadata, compression) {
53
74
  assertLength(encodedByteLength, "encoded payload");
54
- let serializedMetadata;
55
- try {
56
- serializedMetadata = JSON.stringify(metadata);
57
- }
58
- catch {
59
- throw new TypeError("Block metadata must be JSON serializable");
60
- }
61
- if (typeof serializedMetadata !== "string") {
62
- throw new TypeError("Block metadata must be JSON serializable");
63
- }
64
- const metadataByteLength = textEncoder.encode(serializedMetadata).byteLength;
65
- assertLength(metadataByteLength, "metadata");
75
+ const metadataByteLength = encodeCanonicalMetadata(metadata).byteLength;
76
+ assertMetadataLength(metadataByteLength);
66
77
  const maximumStoredPayloadBytes = getCompressionMemoryBound(compression, encodedByteLength).maximumOutputBytes;
67
78
  const total = HEADER_LENGTH + metadataByteLength + maximumStoredPayloadBytes;
68
79
  if (!Number.isSafeInteger(total)) {
@@ -71,8 +82,7 @@ export function maximumPhysicalBlockByteLength(encodedByteLength, metadata, comp
71
82
  return total;
72
83
  }
73
84
  export async function encodeBlock(input, compression = "raw") {
74
- if (input.values.length > 0xffffffff)
75
- throw new RangeError("Too many rows in one block");
85
+ assertBlockRowCount(input.values.length);
76
86
  const encoded = encodeColumn(input);
77
87
  return encodeValidatedPhysicalBlock({
78
88
  type: input.type,
@@ -82,16 +92,39 @@ export async function encodeBlock(input, compression = "raw") {
82
92
  metadata: encoded.metadata,
83
93
  }, compression);
84
94
  }
85
- /** Encodes physical column bytes after validating their complete version-zero layout. */
95
+ /** Encodes physical column bytes after validating their complete canonical v2 layout. */
86
96
  export async function encodePhysicalBlock(input, compression = "raw") {
87
97
  return encodeValidatedPhysicalBlock(validatePhysicalColumn(input), compression);
88
98
  }
89
99
  async function encodeValidatedPhysicalBlock(input, compression) {
100
+ // validatePhysicalColumn constructs this exact closed metadata shape. Avoid serializing,
101
+ // parsing, and serializing it again on every write; the public sizing helper validates its
102
+ // independently supplied metadata because it has no such provenance.
90
103
  const metadata = textEncoder.encode(JSON.stringify(input.metadata));
91
- const stored = await getCodec(compression).compress(input.bytes);
92
- assertLength(metadata.byteLength, "metadata");
104
+ // Checksum before the first possible async yield. The gzip codec also takes its input copy
105
+ // synchronously when called, so caller mutation after encodePhysicalBlock returns cannot make
106
+ // the checksum and compressed bytes observe different physical payloads.
107
+ const logicalChecksum = crc32(input.bytes);
108
+ // Avoid an async yield on raw blocks: validation and the copy into the immutable envelope
109
+ // complete before a caller can mutate a physical input buffer. The gzip codec takes its own
110
+ // input copy before yielding to the native stream.
111
+ let stored;
112
+ try {
113
+ stored =
114
+ compression === "raw"
115
+ ? input.bytes
116
+ : await getCodec(compression).compress(input.bytes, MAX_BLOCK_LENGTH);
117
+ }
118
+ catch (error) {
119
+ if (error instanceof CompressionOutputLimitError) {
120
+ throw new StoredBlockPayloadTooLargeError(error.byteLength);
121
+ }
122
+ throw error;
123
+ }
124
+ assertMetadataLength(metadata.byteLength);
93
125
  assertLength(input.bytes.byteLength, "encoded payload");
94
- assertLength(stored.byteLength, "stored payload");
126
+ assertStoredBlockPayloadByteLength(stored.byteLength);
127
+ const storedChecksum = compression === "raw" ? logicalChecksum : crc32(stored);
95
128
  const output = new Uint8Array(HEADER_LENGTH + metadata.byteLength + stored.byteLength);
96
129
  output.set(MAGIC);
97
130
  const view = new DataView(output.buffer);
@@ -106,30 +139,51 @@ async function encodeValidatedPhysicalBlock(input, compression) {
106
139
  view.setUint32(24, metadata.byteLength, true);
107
140
  view.setUint32(28, input.bytes.byteLength, true);
108
141
  view.setUint32(32, stored.byteLength, true);
109
- view.setUint32(36, crc32(input.bytes), true);
142
+ view.setUint32(36, logicalChecksum, true);
143
+ view.setUint32(40, storedChecksum, true);
110
144
  output.set(metadata, HEADER_LENGTH);
111
145
  output.set(stored, HEADER_LENGTH + metadata.byteLength);
112
146
  view.setUint32(4, crc32(output.subarray(ENVELOPE_CHECKSUM_START, HEADER_LENGTH + metadata.byteLength)), true);
113
147
  return output;
114
148
  }
115
149
  export async function decodeBlock(bytes) {
116
- const decoded = await decodePhysicalBlock(bytes);
150
+ // Materialize raw values before the first async yield. Otherwise a caller could mutate the
151
+ // input after its checksum passes but before decodeColumn reads it. The gzip codec takes an
152
+ // input copy synchronously, so its awaited path has no equivalent gap.
153
+ const description = verifyStoredBlock(bytes);
154
+ const decoded = description.compression === "raw"
155
+ ? validateDecodedPhysicalBlock(bytes.subarray(description.headerLength + description.metadataLength), description)
156
+ : await decodeVerifiedPhysicalBlock(bytes, description);
117
157
  return {
118
158
  description: decoded.description,
119
- column: decodeColumn(decoded.description.type, decoded.column.bytes, decoded.description.rowCount),
159
+ column: decodeValidatedColumn(decoded.column),
120
160
  };
121
161
  }
122
162
  /**
123
163
  * Decompresses and checksum-verifies a block without materializing row values.
124
- * The returned physical byte array is owned by the result.
164
+ *
165
+ * For a raw block, `column.bytes` is a zero-copy view into `bytes`. Both views must be treated as
166
+ * immutable for the lifetime of the result. Gzip results own a new decompressed allocation.
125
167
  */
126
168
  export async function decodePhysicalBlock(bytes) {
127
- const description = inspectBlock(bytes);
128
- const metadataLength = bytes.byteLength - HEADER_LENGTH - description.storedLength;
129
- const stored = bytes.subarray(HEADER_LENGTH + metadataLength);
169
+ const description = verifyStoredBlock(bytes);
170
+ if (description.compression === "raw") {
171
+ return validateDecodedPhysicalBlock(bytes.subarray(description.headerLength + description.metadataLength), description);
172
+ }
173
+ return decodeVerifiedPhysicalBlock(bytes, description);
174
+ }
175
+ async function decodeVerifiedPhysicalBlock(bytes, description) {
176
+ const stored = bytes.subarray(description.headerLength + description.metadataLength);
130
177
  const encoded = await getCodec(description.compression).decompress(stored, description.encodedLength);
131
- if (crc32(encoded) !== description.checksum)
132
- throw new Error("Block checksum mismatch");
178
+ // For raw blocks the stored and logical bytes are identical, and inspectBlock requires both
179
+ // declared checksums to agree. verifyStoredBlock therefore performed the logical check too,
180
+ // avoiding a second full scan on the default codec.
181
+ if (description.compression !== "raw" && crc32(encoded) !== description.checksum) {
182
+ throw new Error("Block logical checksum mismatch");
183
+ }
184
+ return validateDecodedPhysicalBlock(encoded, description);
185
+ }
186
+ function validateDecodedPhysicalBlock(encoded, description) {
133
187
  const column = validatePhysicalColumn({
134
188
  type: description.type,
135
189
  rowCount: description.rowCount,
@@ -145,22 +199,49 @@ export async function decodePhysicalBlock(bytes) {
145
199
  column,
146
200
  };
147
201
  }
202
+ /**
203
+ * Verifies every stored byte without decompressing or copying the payload.
204
+ *
205
+ * Snapshot import and OPFS recovery use this stronger operation before accepting a block.
206
+ * Planning and pruning should continue to use `inspectBlock`, which deliberately reads only the
207
+ * fixed header and metadata envelope.
208
+ */
209
+ export function verifyStoredBlock(bytes) {
210
+ const description = inspectBlock(bytes);
211
+ const storedStart = bytes.byteLength - description.storedLength;
212
+ if (crc32(bytes.subarray(storedStart)) !== description.storedChecksum) {
213
+ throw new Error("Block stored payload checksum mismatch");
214
+ }
215
+ return description;
216
+ }
148
217
  export function inspectBlock(bytes) {
149
- if (bytes.byteLength < HEADER_LENGTH)
218
+ // Only magic and version are common across versions. Dispatch before interpreting any later
219
+ // offsets, so future envelopes can grow or replace the v2 header without weakening its reader.
220
+ if (bytes.byteLength < 10)
150
221
  throw new Error("Truncated block header");
151
222
  if (!MAGIC.every((byte, index) => bytes[index] === byte))
152
223
  throw new Error("Invalid block magic");
153
224
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
154
225
  const formatVersion = view.getUint16(8, true);
155
- if (formatVersion !== VERSION) {
156
- throw new Error(`Unsupported block version ${String(formatVersion)}`);
226
+ switch (formatVersion) {
227
+ case 2:
228
+ return inspectV2Block(bytes);
229
+ default:
230
+ throw new Error(`Unsupported block version ${String(formatVersion)}`);
157
231
  }
232
+ }
233
+ /** Permanent reader for the frozen v2 envelope. */
234
+ function inspectV2Block(bytes) {
235
+ if (bytes.byteLength < HEADER_LENGTH)
236
+ throw new Error("Truncated block header");
237
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
238
+ const formatVersion = view.getUint16(8, true);
158
239
  if (view.getUint16(10, true) !== HEADER_LENGTH)
159
240
  throw new Error("Invalid block header length");
160
241
  const metadataLength = view.getUint32(24, true);
161
242
  const encodedLength = view.getUint32(28, true);
162
243
  const storedLength = view.getUint32(32, true);
163
- assertLength(metadataLength, "metadata");
244
+ assertMetadataLength(metadataLength);
164
245
  assertLength(encodedLength, "encoded payload");
165
246
  assertLength(storedLength, "stored payload");
166
247
  if (HEADER_LENGTH + metadataLength + storedLength !== bytes.byteLength) {
@@ -172,6 +253,8 @@ export function inspectBlock(bytes) {
172
253
  if (envelope !== view.getUint32(4, true)) {
173
254
  throw new Error("Block envelope checksum mismatch");
174
255
  }
256
+ const rowCount = view.getUint32(16, true);
257
+ assertBlockRowCount(rowCount);
175
258
  const type = typesById.get(view.getUint8(12));
176
259
  if (type === undefined)
177
260
  throw new Error("Unknown logical type");
@@ -180,41 +263,83 @@ export function inspectBlock(bytes) {
180
263
  const codecId = view.getUint8(14);
181
264
  const compression = codecsById.get(codecId);
182
265
  if (compression === undefined) {
183
- throw new Error(codecId === 1
184
- ? "Block uses the removed RLE codec; rewrite it as raw or gzip with an older build"
185
- : "Unknown compression codec");
266
+ throw new Error(codecId === 1 ? "Block uses reserved compression codec id 1" : "Unknown compression codec");
186
267
  }
187
268
  if (view.getUint8(15) !== 0)
188
269
  throw new Error("Unsupported mandatory block flags");
189
- const rowCount = view.getUint32(16, true);
270
+ const checksum = view.getUint32(36, true);
271
+ const storedChecksum = view.getUint32(40, true);
272
+ if (compression === "raw" && checksum !== storedChecksum) {
273
+ throw new Error("Raw block checksums do not match");
274
+ }
275
+ if (compression === "raw" && encodedLength !== storedLength) {
276
+ throw new Error("Raw block lengths do not match");
277
+ }
190
278
  const nullCount = view.getUint32(20, true);
191
279
  if (nullCount > rowCount)
192
280
  throw new Error("Null count exceeds row count");
193
281
  let parsedMetadata;
282
+ let metadataJson;
194
283
  try {
195
- parsedMetadata = JSON.parse(textDecoder.decode(bytes.subarray(HEADER_LENGTH, HEADER_LENGTH + metadataLength)));
284
+ metadataJson = textDecoder.decode(bytes.subarray(HEADER_LENGTH, HEADER_LENGTH + metadataLength));
285
+ parsedMetadata = JSON.parse(metadataJson);
196
286
  }
197
287
  catch {
198
288
  throw new Error("Invalid block metadata");
199
289
  }
200
290
  const metadata = parseMetadata(parsedMetadata);
291
+ // There is one canonical metadata representation. Rejecting alternate key order, duplicate
292
+ // keys, unknown fields, and insignificant whitespace keeps byte fixtures meaningful and avoids
293
+ // multiple envelopes describing the same block.
294
+ if (JSON.stringify(metadata) !== metadataJson)
295
+ throw new Error("Non-canonical block metadata");
201
296
  return {
202
297
  formatVersion,
298
+ headerLength: HEADER_LENGTH,
299
+ metadataLength,
203
300
  type,
204
301
  compression,
205
302
  rowCount,
206
303
  nullCount,
207
304
  encodedLength,
208
305
  storedLength,
209
- checksum: view.getUint32(36, true),
306
+ checksum,
307
+ storedChecksum,
210
308
  metadata,
211
309
  };
212
310
  }
311
+ function assertMetadataLength(length) {
312
+ if (!Number.isSafeInteger(length) || length < 0 || length > MAX_METADATA_LENGTH) {
313
+ throw new RangeError("Invalid metadata length");
314
+ }
315
+ }
213
316
  function assertLength(length, name) {
214
317
  if (!Number.isSafeInteger(length) || length < 0 || length > MAX_BLOCK_LENGTH) {
215
318
  throw new RangeError(`Invalid ${name} length`);
216
319
  }
217
320
  }
321
+ function encodeCanonicalMetadata(metadata) {
322
+ let source;
323
+ try {
324
+ source = JSON.stringify(metadata);
325
+ }
326
+ catch {
327
+ throw new TypeError("Block metadata must be canonical JSON");
328
+ }
329
+ if (typeof source !== "string")
330
+ throw new TypeError("Block metadata must be canonical JSON");
331
+ let canonical;
332
+ try {
333
+ canonical = parseMetadata(JSON.parse(source));
334
+ }
335
+ catch {
336
+ throw new TypeError("Block metadata must match the supported canonical shape");
337
+ }
338
+ if (JSON.stringify(canonical) !== source) {
339
+ throw new TypeError("Block metadata must match the supported canonical shape");
340
+ }
341
+ return textEncoder.encode(source);
342
+ }
218
343
  function parseMetadata(value) {
219
344
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
220
345
  throw new Error("Invalid block metadata shape");
@@ -241,4 +366,3 @@ function metadataEquals(left, right) {
241
366
  }
242
367
  return left.zoneMap.min === right.zoneMap.min && left.zoneMap.max === right.zoneMap.max;
243
368
  }
244
- //# sourceMappingURL=block.js.map
@@ -4,4 +4,5 @@
4
4
  * times over, and the tail falls back to single-byte steps.
5
5
  */
6
6
  export declare function crc32(bytes: Uint8Array): number;
7
- //# sourceMappingURL=checksum.d.ts.map
7
+ /** Extends an already-finalized CRC-32 without concatenating or retaining the preceding bytes. */
8
+ export declare function crc32Continue(previous: number, bytes: Uint8Array): number;
@@ -20,7 +20,11 @@ for (let slice = 1; slice < 8; slice += 1) {
20
20
  * times over, and the tail falls back to single-byte steps.
21
21
  */
22
22
  export function crc32(bytes) {
23
- let checksum = 0xffffffff;
23
+ return crc32Continue(0, bytes);
24
+ }
25
+ /** Extends an already-finalized CRC-32 without concatenating or retaining the preceding bytes. */
26
+ export function crc32Continue(previous, bytes) {
27
+ let checksum = (previous ^ 0xffffffff) >>> 0;
24
28
  const length = bytes.byteLength;
25
29
  const wordEnd = length - (length % 8);
26
30
  let index = 0;
@@ -53,4 +57,3 @@ export function crc32(bytes) {
53
57
  }
54
58
  return (checksum ^ 0xffffffff) >>> 0;
55
59
  }
56
- //# sourceMappingURL=checksum.js.map
@@ -1,9 +1,15 @@
1
1
  import type { Compression } from "./types.js";
2
2
  export interface CompressionCodec {
3
3
  readonly id: Compression;
4
- compress(bytes: Uint8Array): Promise<Uint8Array>;
4
+ compress(bytes: Uint8Array, maximumOutputLength?: number): Promise<Uint8Array>;
5
5
  decompress(bytes: Uint8Array, expectedLength: number): Promise<Uint8Array>;
6
6
  }
7
+ /** A caller's explicit compressed-output ceiling was reached before an output join/allocation. */
8
+ export declare class CompressionOutputLimitError extends RangeError {
9
+ readonly byteLength: number;
10
+ readonly maximumOutputLength: number;
11
+ constructor(byteLength: number, maximumOutputLength: number);
12
+ }
7
13
  export interface CompressionMemoryBound {
8
14
  /** Largest returned buffer owned by the caller. */
9
15
  readonly maximumOutputBytes: number;
@@ -21,4 +27,3 @@ export declare const rawCodec: CompressionCodec;
21
27
  export declare function getCompressionMemoryBound(compression: Compression, inputLength: number): CompressionMemoryBound;
22
28
  export declare const gzipCodec: CompressionCodec;
23
29
  export declare function getCodec(id: Compression): CompressionCodec;
24
- //# sourceMappingURL=codecs.d.ts.map