@di-framework/pglite-component 6.0.16 → 6.0.18

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.
package/README.md CHANGED
@@ -130,7 +130,7 @@ The pglite component is published to GitHub Container Registry (GHCR) as an OCI
130
130
 
131
131
  The workflow has four jobs:
132
132
  - `check-source` (`packages: read`) hashes the committed input files (`scripts/source-check.ts`). On a push it skips the build when `ghcr.io/di-framework/pglite-component:src-<hash>` already exists, so commits that do not change the component publish nothing.
133
- - `build` (no write token) runs the same targets as `ci.yml`: `make tools-rust`, `make unit smoke` (which runs `make build`, composing the provider with the engine via `wac plug`), then the publisher crates' clippy and tests.
133
+ - `build` (no write token) runs the same targets as `ci.yml`: `make tools-rust`, `make unit smoke` (which runs `make build`, composing the provider with two instances of the engine via `wac compose`), then the publisher crates' clippy and tests.
134
134
  - `publish` (`packages: write`) downloads the tested `dist/` and runs `pglite-component-publisher`. It never rebuilds or modifies `dist/`. It checks `SHA256SUMS`, requires the export `di-framework:pglite/database@0.2.0` and rejects any import that is not `wasi:*`, then pushes the layer, config and manifest with `oras` under `ghcr.io/di-framework/pglite-component:<commit sha>` only, pulls it back with `wash` by the manifest digest to compare bytes, and only then points the version tag (`0.2.0`, the WIT package version) at that same manifest with `oras tag` and asserts both tags resolve to one digest. A failed check leaves the version tag where it was. The manifest and config carry a fixed `created` time (the publish step clears `SOURCE_DATE_EPOCH`, so `1970-01-01T00:00:00Z`), so publishing an unchanged `di-framework-pglite.wasm` gives the same digest and the pin job has nothing to pin. The `build` job passes the tested wasm sha256 to `publish`, which verifies it before pushing. It also tags the manifest `src-<hash>`.
135
135
  - `pin` (`contents: write`, `pull-requests: write`) opens or updates the PR `bot/pin-pglite-component`, "chore: pin pglite-component to <digest>", which rewrites the digests in this README with `scripts/pin-artifact.ts`. GitHub does not run workflows on a PR that `GITHUB_TOKEN` opened, so a maintainer has to start its checks (close and reopen it) before merging. It also runs when a push's source is already published (a revert to an older source, or a pin PR closed unmerged), so `main`'s pin follows the current source; it opens no PR when `main` already has the digest. On that path the `realign` job (`packages: write`) points the moving version tag back at that source's digest with `scripts/retag.sh`, which leaves a tag that is already there alone. Every tag move is behind the same source guard, so a stale run never retags.
136
136
 
@@ -139,7 +139,7 @@ Tags are movable. `0.2.0` always points to the latest `main` publish for WIT pac
139
139
  ### Artifact location
140
140
 
141
141
  The digest is in **Details** > **Record component digest** > **Step summary**, and in the `pglite-publish-report` artifact (`publish-report.json`).
142
- Published digest: `ghcr.io/di-framework/pglite-component@sha256:9be0cc2f59efea9ade293173ca28bd289a5505eb31b08fb954e718c03316355e`
142
+ Published digest: `ghcr.io/di-framework/pglite-component@sha256:9d5b7c96aa4ad13c1c4de8d0285cf731b9926a47e0a7c680237708d26b439da3`
143
143
 
144
144
  **Note:** The GHCR package must be made public by an org admin after the first publish. Until then, only members with access to di-framework/platform can pull it.
145
145
 
@@ -177,7 +177,7 @@ wasmtime run --dir ./data::/data path/to/app.composed.wasm
177
177
  To use the published OCI component from GHCR instead (pin by digest for reproducibility):
178
178
 
179
179
  ```sh
180
- export PGLITE_REF='ghcr.io/di-framework/pglite-component@sha256:9be0cc2f59efea9ade293173ca28bd289a5505eb31b08fb954e718c03316355e'
180
+ export PGLITE_REF='ghcr.io/di-framework/pglite-component@sha256:9d5b7c96aa4ad13c1c4de8d0285cf731b9926a47e0a7c680237708d26b439da3'
181
181
  wash oci pull "${PGLITE_REF}" pglite.wasm
182
182
  wac plug --plug pglite.wasm path/to/app.wasm -o path/to/app.composed.wasm
183
183
  mkdir -p ./data
@@ -196,8 +196,8 @@ savepoints, `rows-affected`, `close`, and `server-version`. PostgreSQL parameter
196
196
  use `$1`, `$2`, etc. Text parameters are sent with OID 0 (unknown type), allowing
197
197
  PostgreSQL to infer the type from context; comparisons against `uuid`, `timestamptz`,
198
198
  and `jsonb` columns work correctly (see Typed values). `server-version()` reports the bundled engine without
199
- opening a database. Only the `template1` database is currently supported;
200
- other `open-options.database` values are rejected. Requests must fit the engine's
199
+ opening a database (see [Application database](#application-database) for
200
+ `open-options.database`). Requests must fit the engine's
201
201
  input buffer; each serialized result is limited to 64 MiB and stays in memory.
202
202
  Larger results return a PostgreSQL program-limit error. Signal-based statement
203
203
  timeouts are unavailable in this WASI engine: nonzero `statement-timeout-ms`
@@ -211,6 +211,23 @@ and migrations run on embedded PostgreSQL. Compose your component with this
211
211
  provider (see above). The adapter is portable: it uses only
212
212
  `node:async_hooks`, which the framework's component toolchain shims.
213
213
 
214
+ **Toolchain requirement for `int8` parameters past 2^53.** An exact `bigint`
215
+ parameter beyond ±(2^53-1) needs componentize-qjs `0.4.4-di.5` or later, which
216
+ `@di-framework/cli-plugin-platform` 6.2.0 or later installs. Use the default
217
+ runtime, without `--sync` or `--opt-size`. When you install the compiler
218
+ yourself, use the `di` dist-tag (`bun add @di-framework/componentize-qjs@di`):
219
+ npm's `latest` tag of `@di-framework/componentize-qjs` still points at
220
+ `0.4.4-di.1`. Older bindings, and the `--sync` / `--opt-size` runtimes, lower
221
+ WIT `s64` only from Numbers, and such a parameter traps the guest instance
222
+ instead of throwing a catchable error. Safe integers (`number` or `bigint`)
223
+ are sent as Numbers and work on every binding.
224
+
225
+ > **Upgrading.** Before this release, a `bigint` past 2^53 was sent as decimal
226
+ > text. It is now sent as an exact `int8` integer. If your guest binds such
227
+ > values, update the CLI plugin to 6.2.0 or later (componentize-qjs
228
+ > `0.4.4-di.5`) before you update this package. Guests that bind only safe
229
+ > integers need no change.
230
+
214
231
  ```ts
215
232
  import type { SqlDatabase } from '@di-framework/repo';
216
233
  import { createWasmPgliteDatabase } from '@di-framework/pglite-component';
@@ -233,8 +250,8 @@ handle and share it: every call goes through one async mutex.
233
250
 
234
251
  - **Transactions.** `transaction(fn)` runs the provider's `begin` and `commit`, and `rollback` when `fn` throws. Statements issued from inside `fn`, through the callback view or the outer handle, join the transaction; other tasks wait for it. Inside `fn`, prefer the callback view: while the transaction is open it joins from any async context, including a worker loop or `AsyncResource` created earlier, and Wasm-guest code that the `node:async_hooks` shim does not instrument (native async generators). The outer handle joins only from `fn`'s own async context. From any other context it waits for the mutex, so awaiting it from `fn` deadlocks. A nested `transaction()` opens a savepoint. When it throws, only its own work rolls back, and the outer transaction can continue. Its work commits only with the outer transaction. Sibling savepoints started concurrently run one at a time. A plain statement issued through a view while a sibling savepoint is open joins that savepoint and rolls back with it: in `Promise.all([tx.transaction(failing), (async () => { await work(); await tx.run(...); })()])`, a `run` issued while the savepoint is open lands inside it, so await the savepoint first when the statement must survive it. Statements issued concurrently inside one transaction reach the connection one at a time, also with bindings that return promises. `transaction()` on a view, called from another async context while a sibling savepoint is open, waits for that sibling to finish; if the sibling awaits that call, it waits forever. Joining lasts only while a transaction is open. A savepoint's callback view kept after the savepoint returns joins the nearest open ancestor while the root transaction is still open, and its work commits or rolls back with that ancestor. Once the root transaction has ended, a retained callback view, like a timer scheduled inside `fn`, waits for the mutex like any other caller and never runs inside another task's transaction.
235
252
  - **Placeholders.** `?` placeholders become `$1`, `$2`, ... in order (`toPgPlaceholders`). The rewrite skips `'...'` and `E'...'` strings, `"..."` identifiers, `$tag$...$tag$` bodies, `--` and `/* */` comments, and the jsonb operators `?|` and `?&`. Because of that, `?||` reads as `?|` followed by `|`: put a space in the concatenation (`? || 'x'`). The bare jsonb `?` operator looks like a placeholder: use `jsonb_exists(col, ?)` in a statement with parameters. A statement run without parameters is not rewritten. You can write `$n` yourself, but do not mix `$n` with `?` in one statement.
236
- - **Parameters.** `string` is sent as text, and PostgreSQL infers its type from context, so a UUID or timestamp string works against `uuid` and `timestamptz` columns. A safe integer is `int8`, another number is `float8`, a `boolean` is `bool`, and `Uint8Array`, `ArrayBuffer` and typed arrays are `bytea`. A `Date` is sent as an RFC 3339 timestamp. Plain objects and arrays are sent as JSON, so a JavaScript array is not a PostgreSQL array: pass a literal such as `'{a,b}'`. A `bigint` outside the safe integer range is sent as decimal text, so it is not rounded. A `number` that is an integer outside the safe range throws a `TypeError`, because it has already lost precision: pass a `bigint` or a decimal string for `int8`. NaN and Infinity are valid `float8` values and are sent as `real`.
237
- - **Results.** Values match the `wasmcloud:postgres` codec in `@di-framework/repo`, so repositories behave the same on both backends. `json` and `jsonb` are parsed. `timestamptz` is an ISO string with milliseconds (`2024-01-15T10:30:00.123Z`, microseconds truncated); `infinity` and BC dates are returned as PostgreSQL prints them. An `int8` outside the safe integer range is a decimal string, but only when the binding lifts WIT `s64` as a `bigint`; a binding that lifts it as a JavaScript number (componentize-qjs lowers `s64` from numbers) has already lost the precision, so cast large `int8` columns to `text` in the query (`id::text`) when exact values matter (#153). `bool` is a boolean, `bytea` is a `Uint8Array`, and `timestamp` without zone, `date`, `uuid`, `numeric` and arrays are strings.
253
+ - **Parameters.** `string` is sent as text, and PostgreSQL infers its type from context, so a UUID or timestamp string works against `uuid` and `timestamptz` columns. A safe integer (`number` or `bigint`) is `int8`, sent as a JavaScript number, another number is `float8`, a `boolean` is `bool`, and `Uint8Array`, `ArrayBuffer` and typed arrays are `bytea`. A `Date` is sent as an RFC 3339 timestamp. Plain objects and arrays are sent as JSON, so a JavaScript array is not a PostgreSQL array: pass a literal such as `'{a,b}'`. A `bigint` past 2^53 that fits `int8` is sent as an exact `int8` with a `BigInt` payload, which needs componentize-qjs `0.4.4-di.5` or later (see the requirement above); one outside the `int8` range is sent as decimal text, so a `numeric` column receives it exactly. A `number` that is an integer outside the safe range throws a `TypeError`, because it has already lost precision: pass a `bigint` or a decimal string for `int8`. NaN and Infinity are valid `float8` values and are sent as `real`.
254
+ - **Results.** Values match the `wasmcloud:postgres` codec in `@di-framework/repo`, so repositories behave the same on both backends. `json` and `jsonb` are parsed. `timestamptz` is an ISO string with milliseconds (`2024-01-15T10:30:00.123Z`, microseconds truncated); `infinity` and BC dates are returned as PostgreSQL prints them. An `int8` outside the safe integer range is an exact decimal string when the binding lifts WIT `s64` as a `bigint` (componentize-qjs `0.4.4-di.5` or later with the default runtime, or jco). Older componentize-qjs releases and the `--sync` / `--opt-size` runtimes lift `s64` as a Number, which has already rounded it, so cast large `int8` columns to `text` (`id::text`) on those. `bool` is a boolean, `bytea` is a `Uint8Array`, and `timestamp` without zone, `date`, `uuid`, `numeric` and arrays are strings.
238
255
  - **Errors.** Failures throw `WasmPgliteError` with the WIT `tag`. A PostgreSQL error keeps its SQLSTATE in `error.code` (`'23505'` for a unique violation), and the message starts with `PostgreSQL <code>`, so `isUniqueViolation` from `@di-framework/repo` works too.
239
256
 
240
257
  Pass `module` or `loadModule` to supply the interface yourself (for tests), and
@@ -360,11 +377,90 @@ libraries per engine instance. pgcrypto resolves before that table and never
360
377
  uses a slot. A `LOAD` of a new library after the table is full fails with
361
378
  `dlopen table is full` and leaves the session usable.
362
379
 
380
+ ### Application database
381
+
382
+ Application data lives in the `postgres` database on data directories this
383
+ provider initializes. Directories created before this release keep it in
384
+ `template1` (#212). With `open-options.database` unset:
385
+
386
+ | Data directory | Default database |
387
+ | --- | --- |
388
+ | Initialized by this provider: `<root>/data/di-framework-database` contains `postgres` | `postgres` |
389
+ | No marker: created before this release (6.0.16 and earlier) | `template1`, unchanged |
390
+
391
+ Upgrading the provider never moves data: an existing directory keeps opening
392
+ `template1`, and it never gets the marker. Fresh directories open `postgres`
393
+ from their first session on.
394
+
395
+ **Do not roll back below this release for a directory it created.** Providers
396
+ from before this release (6.0.16 and earlier) ignore the marker and open
397
+ `template1`. On a directory created by this release, that database is empty:
398
+ the application sees no data, may re-run its migrations, and writes into
399
+ `template1`. Upgrading again brings the `postgres` data back, and everything
400
+ written during the rollback stays stranded in `template1`. To roll back,
401
+ either roll back to a build that reads the marker, or restore the backup of
402
+ the root taken before the upgrade.
403
+
404
+ Explicit `open-options.database` values:
405
+
406
+ - `postgres` and `template1` are accepted. initdb creates both, so they exist
407
+ in every complete directory; on an older directory, `postgres` exists but is
408
+ empty. Before the engine starts, the provider checks that the database's
409
+ fixed-OID directory exists (`base/1` for `template1`, `base/5` for
410
+ `postgres`). If it does not, `open` fails with `open-failed`
411
+ (`database "postgres" has no data directory at base/5 ...`) and the same
412
+ component instance can still open `template1`. This is a filesystem check,
413
+ not a catalog lookup, so it does not catch every case:
414
+ - If initdb crashed while creating `postgres`, after copying `base/5` but
415
+ before committing the database, the check passes and an explicit
416
+ `postgres` open still stops the engine with
417
+ `FATAL: database "postgres" does not exist`. Only an initdb that crashed
418
+ partway through leaves a directory in this state, and with or without the
419
+ clean error such a directory is unfinished: re-create it (remove the root)
420
+ or restore it from a backup, do not use it.
421
+ - A `postgres` database that was dropped and re-created (new OID) or moved
422
+ with `ALTER DATABASE ... SET TABLESPACE` has no `base/5`, so the provider
423
+ refuses to open it although it exists.
424
+ - `template0` is always rejected: the single-user engine would not enforce
425
+ `datallowconn`, so opening it would make the pristine template writable.
426
+ - Any other name is rejected. The list is fixed because the engine picks its
427
+ database before its backend starts, so it cannot look the name up first.
428
+
429
+ The embedded backend stays connected to the database its first `open` chose.
430
+ A later `open` in the same component instance that resolves to a different
431
+ database fails with `open-failed`; restart the component to switch.
432
+
433
+ The marker is part of the data directory. Back it up and restore it with the
434
+ rest of PGDATA. Without it, a directory falls back to `template1`, and an
435
+ application whose tables are in `postgres` finds them missing; the provider
436
+ cannot detect this. Do not add the marker by hand to a directory whose data
437
+ is in `template1`. To move a legacy directory to `postgres`, export the data
438
+ through the application and import it into a fresh directory, as described
439
+ in [Upgrading across PostgreSQL major versions](#upgrading-across-postgresql-major-versions)
440
+ (no `pg_dump` is bundled).
441
+
442
+ Initialization runs in a separate, single-use engine instance bundled into
443
+ the provider: initdb's post-bootstrap step runs in `template1` and the
444
+ single-user backend cannot change databases afterwards. That instance creates
445
+ the cluster and the marker. The serving instance then boots on the finished
446
+ cluster in the selected database and verifies it with `current_database()`
447
+ before the first session is handed out.
448
+
449
+ Cost: the bootstrap instance runs only when a directory is created, but the
450
+ component model cannot drop an instance, so its linear memory and its open
451
+ files stay held until the process exits. The first start on a new directory
452
+ peaks at about 690 MB RSS (about 106 MB more than a single engine) and holds
453
+ about 43 more host file handles under the root. Later starts on an existing
454
+ directory cost the same as before (about 264 MB in the smoke consumer).
455
+ Restart the component after creating a directory if that memory matters.
456
+
363
457
  ## Persistence and ownership
364
458
 
365
459
  For an `open("/data/orders", ...)` call:
366
460
 
367
- - `/data/orders/data/` is PGDATA (catalogs, tables, WAL and configuration).
461
+ - `/data/orders/data/` is PGDATA (catalogs, tables, WAL and configuration,
462
+ plus the `di-framework-database` marker on directories this provider
463
+ initialized; see [Application database](#application-database)).
368
464
  - `/data/orders/runtime/` contains bundled initialization/support files.
369
465
  - Bootstrap and shared-memory scratch files also stay beneath `/data/orders`.
370
466
 
@@ -440,7 +536,7 @@ for both the export and the import. PostgreSQL 18 is tracked in #401.
440
536
  - `js/wit.d.ts`: generated TypeScript view of the `database` interface.
441
537
  - `src/lib.rs`: public WIT resources and connection lifecycle.
442
538
  - `src/pgwire.rs`: PostgreSQL message encoding and typed rows, over memory.
443
- - `src/engine.rs`: persistent filesystem setup and exclusive engine access.
539
+ - `src/engine.rs`: persistent filesystem setup, exclusive engine access, and the application database choice (marker, crash windows).
444
540
  - `engine/bridge.wat`: private canonical ABI bridge to the core engine.
445
541
  - `engine/build-pgcrypto.sh`: builds OpenSSL libcrypto and pgcrypto in the engine container and generates pgcrypto's static `dlsym` table; `scripts/patch-engine-source.ts` links both into the engine. The libcrypto build is cached under a key of the OpenSSL source tarball's sha256, the Configure options and `SOURCE_DATE_EPOCH`, so an OpenSSL version bump always rebuilds it.
446
542
  - `scripts/patch-engine-source.ts`: also replaces the upstream `dlopen` emulation's unbounded table (`dltab`) with the bounded one described under [Extensions](#extensions), and keeps pgcrypto's hook ahead of it.
@@ -448,5 +544,6 @@ for both the export and the import. PostgreSQL 18 is tracked in #401.
448
544
  - `scripts/build-engine.ts`: builds the engine source tree (pinned tag, checked commit, checked patches) and runs the wasipg container build; `scripts/check-engine-patch.ts` (`make check-engine-patch`) runs the same source steps without the build.
449
545
  - `scripts/prepare-engine.ts`: pinned engine preparation and componentization (all build scripts are Bun TypeScript; see `scripts/`).
450
546
  - `wit/deps/pglite-engine/engine.wit`: internal interface, composed away at build.
547
+ - `wit/deps/pglite-engine-bootstrap/bootstrap.wit`: internal interface of the single-use initdb engine instance; `scripts/build.ts` (`providerWac`) feeds it from a second instance of the same engine component.
451
548
 
452
549
  Engine provenance and modifications are described in `engine/NOTICE`.
@@ -1,6 +1,6 @@
1
1
  component: di-framework:pglite@0.2.0
2
2
  profile: release
3
- built: 2026-10-16T17:04:13Z
3
+ built: 2026-10-26T17:05:09Z
4
4
  rustc: rustc 1.97.1 (8bab26f4f 2026-07-14)
5
5
  target: wasm32-wasip2
6
6
  wasm-tools: wasm-tools 1.258.0 (5c6d31c78 2026-08-24)
@@ -13,4 +13,4 @@ engine-patch: engine/patches/postgresql-17.11-wasi.patch (sha256 6beb64a561ca
13
13
  engine-recipe: https://github.com/moznion/wasipg/tree/fa290be67946c0d3c4540bd1400b11e193d874e1
14
14
  adapter-sha256: e51dd61669a20daca34befe7fdadf1ac84e4795a2c84a8400818ee1de5373ccc
15
15
  engine-sha256: eb374344cff24c4cedacdad05a636a3b75b5eb1c4e55aa1a3ff9930be2ebc328
16
- size: 20168533 bytes
16
+ size: 20176453 bytes
package/dist/SHA256SUMS CHANGED
@@ -1,4 +1,4 @@
1
- a98a0476f61d539c72458be572d5705c8a237a49d51329784b65d564fab1b657 di-framework-pglite.wasm
2
- 6053154ec1bc94de63ce4f4f4cce5922ef57dce794c2dde326f5845b8982d35f di-framework-pglite.wit
3
- f76f82266b1f741e10c5c89e6c30f620571adba6bb228a33324681d2dc1822b3 BUILD-INFO.txt
1
+ 04cea641db9ac90a954fefd7783100175ce15bb2cc2e6fc47660fb4090eb2663 di-framework-pglite.wasm
2
+ fcce3740d3bc8501368fa6c4686eb626a1e6d4d15d017817de273dfd77afcab9 di-framework-pglite.wit
3
+ dd70a47b2deeac075d75545fd9c72876158291c8eeaccb1b713082d7ee25c801 BUILD-INFO.txt
4
4
  443fd294671eb64972556656d3ee871e48448ad1169c012d18aa868568f628c1 THIRD-PARTY-NOTICES.txt
Binary file
@@ -1,6 +1,7 @@
1
1
  package root:component;
2
2
 
3
3
  world root {
4
+ import wasi:random/random@0.2.3;
4
5
  import wasi:io/poll@0.2.9;
5
6
  import wasi:clocks/monotonic-clock@0.2.9;
6
7
  import wasi:clocks/wall-clock@0.2.9;
@@ -18,11 +19,17 @@ world root {
18
19
  import wasi:cli/terminal-stderr@0.2.9;
19
20
  import wasi:filesystem/types@0.2.9;
20
21
  import wasi:filesystem/preopens@0.2.9;
21
- import wasi:random/random@0.2.3;
22
22
 
23
23
  export di-framework:pglite/types@0.2.0;
24
24
  export di-framework:pglite/database@0.2.0;
25
25
  }
26
+ package wasi:random@0.2.3 {
27
+ interface random {
28
+ get-random-bytes: func(len: u64) -> list<u8>;
29
+ }
30
+ }
31
+
32
+
26
33
  package wasi:io@0.2.9 {
27
34
  interface poll {
28
35
  resource pollable {
@@ -40,16 +47,16 @@ package wasi:io@0.2.9 {
40
47
 
41
48
  resource input-stream {
42
49
  read: func(len: u64) -> result<list<u8>, stream-error>;
43
- subscribe: func() -> pollable;
44
50
  blocking-read: func(len: u64) -> result<list<u8>, stream-error>;
51
+ subscribe: func() -> pollable;
45
52
  }
46
53
 
47
54
  resource output-stream {
48
55
  check-write: func() -> result<u64, stream-error>;
49
56
  write: func(contents: list<u8>) -> result<_, stream-error>;
57
+ blocking-write-and-flush: func(contents: list<u8>) -> result<_, stream-error>;
50
58
  blocking-flush: func() -> result<_, stream-error>;
51
59
  subscribe: func() -> pollable;
52
- blocking-write-and-flush: func(contents: list<u8>) -> result<_, stream-error>;
53
60
  }
54
61
 
55
62
  variant stream-error {
@@ -64,15 +71,15 @@ package wasi:clocks@0.2.9 {
64
71
  interface monotonic-clock {
65
72
  use wasi:io/poll@0.2.9.{pollable};
66
73
 
67
- type duration = u64;
68
-
69
74
  type instant = u64;
70
75
 
71
- subscribe-duration: func(when: duration) -> pollable;
76
+ type duration = u64;
72
77
 
73
78
  now: func() -> instant;
74
79
 
75
80
  subscribe-instant: func(when: instant) -> pollable;
81
+
82
+ subscribe-duration: func(when: duration) -> pollable;
76
83
  }
77
84
  interface wall-clock {
78
85
  record datetime {
@@ -135,42 +142,47 @@ package wasi:cli@0.2.9 {
135
142
 
136
143
  package wasi:filesystem@0.2.9 {
137
144
  interface types {
138
- use wasi:io/streams@0.2.9.{input-stream, output-stream};
139
- use wasi:clocks/wall-clock@0.2.9.{datetime};
140
145
  use wasi:io/error@0.2.9.{error};
146
+ use wasi:clocks/wall-clock@0.2.9.{datetime};
147
+ use wasi:io/streams@0.2.9.{input-stream, output-stream};
141
148
 
142
149
  resource descriptor {
143
150
  read-via-stream: func(offset: filesize) -> result<input-stream, error-code>;
144
151
  write-via-stream: func(offset: filesize) -> result<output-stream, error-code>;
145
152
  append-via-stream: func() -> result<output-stream, error-code>;
153
+ advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>;
154
+ sync-data: func() -> result<_, error-code>;
146
155
  get-flags: func() -> result<descriptor-flags, error-code>;
156
+ get-type: func() -> result<descriptor-type, error-code>;
157
+ set-size: func(size: filesize) -> result<_, error-code>;
158
+ read: func(length: filesize, offset: filesize) -> result<tuple<list<u8>, bool>, error-code>;
159
+ write: func(buffer: list<u8>, offset: filesize) -> result<filesize, error-code>;
147
160
  read-directory: func() -> result<directory-entry-stream, error-code>;
148
161
  sync: func() -> result<_, error-code>;
149
162
  create-directory-at: func(path: string) -> result<_, error-code>;
150
163
  stat: func() -> result<descriptor-stat, error-code>;
151
164
  stat-at: func(path-flags: path-flags, path: string) -> result<descriptor-stat, error-code>;
152
165
  open-at: func(path-flags: path-flags, path: string, open-flags: open-flags, %flags: descriptor-flags) -> result<descriptor, error-code>;
153
- rename-at: func(old-path: string, new-descriptor: borrow<descriptor>, new-path: string) -> result<_, error-code>;
154
- metadata-hash: func() -> result<metadata-hash-value, error-code>;
155
- metadata-hash-at: func(path-flags: path-flags, path: string) -> result<metadata-hash-value, error-code>;
156
- advise: func(offset: filesize, length: filesize, advice: advice) -> result<_, error-code>;
157
- sync-data: func() -> result<_, error-code>;
158
- get-type: func() -> result<descriptor-type, error-code>;
159
- set-size: func(size: filesize) -> result<_, error-code>;
160
- read: func(length: filesize, offset: filesize) -> result<tuple<list<u8>, bool>, error-code>;
161
- write: func(buffer: list<u8>, offset: filesize) -> result<filesize, error-code>;
162
166
  readlink-at: func(path: string) -> result<string, error-code>;
163
167
  remove-directory-at: func(path: string) -> result<_, error-code>;
168
+ rename-at: func(old-path: string, new-descriptor: borrow<descriptor>, new-path: string) -> result<_, error-code>;
164
169
  symlink-at: func(old-path: string, new-path: string) -> result<_, error-code>;
165
170
  unlink-file-at: func(path: string) -> result<_, error-code>;
166
- }
167
-
168
- resource directory-entry-stream {
169
- read-directory-entry: func() -> result<option<directory-entry>, error-code>;
171
+ metadata-hash: func() -> result<metadata-hash-value, error-code>;
172
+ metadata-hash-at: func(path-flags: path-flags, path: string) -> result<metadata-hash-value, error-code>;
170
173
  }
171
174
 
172
175
  type filesize = u64;
173
176
 
177
+ enum advice {
178
+ normal,
179
+ sequential,
180
+ random,
181
+ will-need,
182
+ dont-need,
183
+ no-reuse,
184
+ }
185
+
174
186
  enum error-code {
175
187
  access,
176
188
  would-block,
@@ -211,6 +223,10 @@ package wasi:filesystem@0.2.9 {
211
223
  cross-device,
212
224
  }
213
225
 
226
+ resource directory-entry-stream {
227
+ read-directory-entry: func() -> result<option<directory-entry>, error-code>;
228
+ }
229
+
214
230
  flags descriptor-flags {
215
231
  read,
216
232
  write,
@@ -231,6 +247,15 @@ package wasi:filesystem@0.2.9 {
231
247
  socket,
232
248
  }
233
249
 
250
+ record directory-entry {
251
+ %type: descriptor-type,
252
+ name: string,
253
+ }
254
+
255
+ flags path-flags {
256
+ symlink-follow,
257
+ }
258
+
234
259
  type link-count = u64;
235
260
 
236
261
  record descriptor-stat {
@@ -242,10 +267,6 @@ package wasi:filesystem@0.2.9 {
242
267
  status-change-timestamp: option<datetime>,
243
268
  }
244
269
 
245
- flags path-flags {
246
- symlink-follow,
247
- }
248
-
249
270
  flags open-flags {
250
271
  create,
251
272
  directory,
@@ -258,20 +279,6 @@ package wasi:filesystem@0.2.9 {
258
279
  upper: u64,
259
280
  }
260
281
 
261
- record directory-entry {
262
- %type: descriptor-type,
263
- name: string,
264
- }
265
-
266
- enum advice {
267
- normal,
268
- sequential,
269
- random,
270
- will-need,
271
- dont-need,
272
- no-reuse,
273
- }
274
-
275
282
  filesystem-error-code: func(err: borrow<error>) -> option<error-code>;
276
283
  }
277
284
  interface preopens {
@@ -282,13 +289,6 @@ package wasi:filesystem@0.2.9 {
282
289
  }
283
290
 
284
291
 
285
- package wasi:random@0.2.3 {
286
- interface random {
287
- get-random-bytes: func(len: u64) -> list<u8>;
288
- }
289
- }
290
-
291
-
292
292
  package di-framework:pglite@0.2.0 {
293
293
  interface types {
294
294
  record pg-error {
@@ -51,7 +51,13 @@ export interface WasmPgliteConnection {
51
51
  }
52
52
  /** The `open-options` record. Every field is optional. */
53
53
  export interface WasmPgliteOpenOptions {
54
- /** Session database. The provider currently accepts only `template1`. */
54
+ /**
55
+ * Session database: `postgres` or `template1`. Unset selects the data
56
+ * directory's default: `postgres` for a directory the provider initialized
57
+ * (it writes a `di-framework-database` marker in PGDATA), `template1` for
58
+ * one created by an earlier release. The provider rejects `template0` and
59
+ * any other name.
60
+ */
55
61
  database?: string;
56
62
  /** Reserved by the provider: only `0` (or unset) is accepted. */
57
63
  statementTimeoutMs?: number;
@@ -87,9 +93,17 @@ export declare function unwrapWasmPgliteResult<T>(value: T | WasmResult<T>): T;
87
93
  /**
88
94
  * Converts a bound JavaScript parameter into a `value` variant.
89
95
  *
90
- * - `bigint` in the safe integer range becomes `integer`; a larger one is sent
91
- * as `text` (OID 0), so PostgreSQL parses it for an `int8`/`numeric` column
92
- * without rounding through a double.
96
+ * - Integers are sent in three bands:
97
+ * - A safe integer (`number` or `bigint`, |v| <= 2^53-1) becomes `integer`
98
+ * with a `number` payload. Every componentize-qjs release, including the
99
+ * `--sync` / `--opt-size` runtimes, and jco lower WIT `s64` from it.
100
+ * - A `bigint` past 2^53 that fits `int8` becomes `integer` with a `bigint`
101
+ * payload, so it reaches PostgreSQL exact. That needs a binding that lowers
102
+ * `s64` from `BigInt`: componentize-qjs 0.4.4-di.5 or later (default
103
+ * runtime) or jco. Older componentize-qjs bindings trap on it.
104
+ * - A `bigint` outside the `int8` range is sent as `text` (OID 0), so
105
+ * PostgreSQL parses it for a `numeric` column instead of the binding
106
+ * wrapping it modulo 2^64.
93
107
  * - A `number` that is an integer outside the safe range throws a `TypeError`
94
108
  * (pass a `bigint` or string for `int8`); other non-safe numbers, including
95
109
  * NaN and Infinity, are `real` (`float8`).
@@ -84,12 +84,22 @@ export function unwrapWasmPgliteResult(value) {
84
84
  }
85
85
  const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
86
86
  const MIN_SAFE = BigInt(Number.MIN_SAFE_INTEGER);
87
+ const MAX_INT8 = 2n ** 63n - 1n;
88
+ const MIN_INT8 = -(2n ** 63n);
87
89
  /**
88
90
  * Converts a bound JavaScript parameter into a `value` variant.
89
91
  *
90
- * - `bigint` in the safe integer range becomes `integer`; a larger one is sent
91
- * as `text` (OID 0), so PostgreSQL parses it for an `int8`/`numeric` column
92
- * without rounding through a double.
92
+ * - Integers are sent in three bands:
93
+ * - A safe integer (`number` or `bigint`, |v| <= 2^53-1) becomes `integer`
94
+ * with a `number` payload. Every componentize-qjs release, including the
95
+ * `--sync` / `--opt-size` runtimes, and jco lower WIT `s64` from it.
96
+ * - A `bigint` past 2^53 that fits `int8` becomes `integer` with a `bigint`
97
+ * payload, so it reaches PostgreSQL exact. That needs a binding that lowers
98
+ * `s64` from `BigInt`: componentize-qjs 0.4.4-di.5 or later (default
99
+ * runtime) or jco. Older componentize-qjs bindings trap on it.
100
+ * - A `bigint` outside the `int8` range is sent as `text` (OID 0), so
101
+ * PostgreSQL parses it for a `numeric` column instead of the binding
102
+ * wrapping it modulo 2^64.
93
103
  * - A `number` that is an integer outside the safe range throws a `TypeError`
94
104
  * (pass a `bigint` or string for `int8`); other non-safe numbers, including
95
105
  * NaN and Infinity, are `real` (`float8`).
@@ -101,14 +111,20 @@ export function toWasmPgValue(value) {
101
111
  return { tag: 'null' };
102
112
  switch (typeof value) {
103
113
  case 'bigint':
104
- // componentize-qjs lowers WIT s64 from JS numbers (f64), not BigInt (#153).
114
+ // Safe values go as Numbers so bindings that lower s64 only from Numbers
115
+ // (componentize-qjs before 0.4.4-di.5, --sync, --opt-size) keep working.
105
116
  if (value >= MIN_SAFE && value <= MAX_SAFE) {
106
117
  return { tag: 'integer', val: Number(value) };
107
118
  }
119
+ // Past 2^53 only a BigInt is exact (componentize-qjs 0.4.4-di.5+, jco).
120
+ if (value >= MIN_INT8 && value <= MAX_INT8) {
121
+ return { tag: 'integer', val: value };
122
+ }
108
123
  return { tag: 'text', val: value.toString() };
109
124
  case 'number':
110
125
  if (Number.isSafeInteger(value)) {
111
- return { tag: 'integer', val: value };
126
+ // `+ 0` turns -0 into 0, which is the int8 PostgreSQL stores anyway.
127
+ return { tag: 'integer', val: value + 0 };
112
128
  }
113
129
  if (Number.isInteger(value)) {
114
130
  // An integral double past 2^53 has already lost its low bits, and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@di-framework/pglite-component",
3
- "version": "6.0.16",
3
+ "version": "6.0.18",
4
4
  "type": "module",
5
5
  "description": "Embedded PostgreSQL component with persistent WASI storage and no TCP, plus its SqlDatabase adapter for @di-framework/repo.",
6
6
  "license": "(MIT OR Apache-2.0)",
@@ -0,0 +1,22 @@
1
+ package di-framework:pglite-engine-bootstrap@0.1.0;
2
+
3
+ /// Private: a second, single-use instance of the bundled engine that runs
4
+ /// initdb on an empty data directory (#212).
5
+ ///
6
+ /// The functions match `di-framework:pglite-engine/engine@0.1.0`; the build
7
+ /// composes this import from a second instance of the same engine component
8
+ /// (see `scripts/build.ts`). Like the engine import, it is never a host import
9
+ /// in the final artifact.
10
+ ///
11
+ /// Why a second instance: the engine's first backend session after initdb
12
+ /// runs initdb's post-bootstrap SQL, which creates the `postgres` database
13
+ /// from a `template1` session, and a single-user backend cannot change
14
+ /// databases afterwards. The provider therefore initializes the cluster here
15
+ /// and serves every session, the first included, from the main engine
16
+ /// instance booted on the existing cluster with the selected database.
17
+ interface bootstrap {
18
+ set-env: func(name: string, value: string) -> s32;
19
+ start: func(arguments: list<string>) -> s32;
20
+ initdb: func() -> s32;
21
+ backend: func();
22
+ }
package/wit/world.wit CHANGED
@@ -91,7 +91,15 @@ interface types {
91
91
  /// Options accepted by `open`. Every field is optional; `none` means the
92
92
  /// documented default.
93
93
  record open-options {
94
- /// Database name for the session. Only `template1` is currently supported.
94
+ /// Database name for the session: `postgres` or `template1`.
95
+ ///
96
+ /// `none` selects the data directory's default. A directory this
97
+ /// provider initializes keeps application data in `postgres` and records
98
+ /// that with a `di-framework-database` marker in PGDATA. A directory
99
+ /// without the marker (one created by an earlier release) keeps using
100
+ /// `template1`, unchanged. `template0` and every other name fail `open`.
101
+ /// The engine stays in the database its first `open` selected until the
102
+ /// component restarts; a later `open` naming another one fails.
95
103
  database: option<string>,
96
104
  /// Reserved: WASI lacks the signal timer used for query cancellation.
97
105
  /// Only none or zero is supported; nonzero values fail open.
@@ -174,10 +182,11 @@ interface database {
174
182
  /// Kept symmetric with the sqlite-component: `types` is exported explicitly
175
183
  /// so the component is self-contained for `wac plug`.
176
184
  ///
177
- /// The private engine import is resolved during the provider build. The
185
+ /// The private engine imports are resolved during the provider build. The
178
186
  /// shipped artifact has only WASI imports, including filesystem and randomness.
179
187
  world pglite-provider {
180
188
  import di-framework:pglite-engine/engine@0.1.0;
189
+ import di-framework:pglite-engine-bootstrap/bootstrap@0.1.0;
181
190
  export types;
182
191
  export database;
183
192
  }