docs(experiment): oliphaunt/pglite-oxide lightweight dev-mode harness (WIN-2130)

Standalone dev/embedded-db harness (boots embedded pglite Postgres, execs windmill against it; plus a schema-compat probe) and a write-up of the experiment. The enabling migration-bootstrap fix landed separately in #9970 (now on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-06 19:03:11 +00:00
co-authored by Claude Opus 4.8
parent 97d14d979f
commit 8e7c1ce8bb
7 changed files with 5115 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
/target
.wm-embedded-pgdata/
.schema-compat-pgdata/
+4703
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -0,0 +1,25 @@
# Standalone experiment crate (WIN-2130). Intentionally NOT a member of the
# backend workspace: it pulls the heavy wasmer/wasix tree and pins alpha
# versions, which must not leak into the main backend Cargo.lock.
[package]
name = "wm-embedded-db"
version = "0.1.0"
edition = "2021"
publish = false
[[bin]]
name = "wm-embedded-db"
path = "src/main.rs"
[[bin]]
name = "schema-compat"
path = "src/schema_compat.rs"
[dependencies]
pglite-oxide = "0.5"
anyhow = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "postgres", "migrate"] }
# Detach from any parent workspace so this keeps its own isolated lockfile.
[workspace]
+54
View File
@@ -0,0 +1,54 @@
# wm-embedded-db — oliphaunt/pglite-oxide dev-mode harness (WIN-2130)
Experiment harness for running Windmill against an **embedded Postgres**
([`pglite-oxide`](https://crates.io/crates/pglite-oxide), the crates.io release of
[`oliphaunt`](https://github.com/f0rr0/oliphaunt)) — Postgres 17.5 compiled to
WASIX, no Docker, no Postgres install.
See the write-up and conclusions in
[`docs/experiments/oliphaunt-lightweight-dev-mode.md`](../../docs/experiments/oliphaunt-lightweight-dev-mode.md).
**Short version: it works** — with the migration-bootstrap fix (merged in #9970),
a full Windmill server+worker boots on the embedded single-connection database and
runs scripts and flows.
This crate is intentionally **not** a member of the backend workspace: it pulls
the heavy `wasmer`/`wasix` tree and pins alpha versions, which must not leak into
the main backend `Cargo.lock`. The committed `Cargo.lock` here carries the
required `virtual-net = 0.702.0-alpha.3` pin (without it, `wasmer-wasix` fails to
compile — see the write-up).
## Binaries
### `wm-embedded-db` (launcher)
Boots an embedded Postgres, exports `DATABASE_URL`, and execs the command after
`--` with that env set. The DB lives in this parent process and shuts down when
the child exits.
```bash
cd dev/embedded-db
# Just boot the DB and print its URL, then idle (Ctrl-C to stop):
cargo run
# Boot the DB, then run the windmill binary against it (requires the migration
# fix from #9970, now on main):
cargo run -- ../../backend/target/debug/windmill
# -> windmill migrates, boots server+worker, and runs scripts/flows on pglite.
# Run the windmill binary with DATABASE_CONNECTIONS=1 to match pglite's model.
```
Env:
- `WM_PGDATA` — data dir (default `./.wm-embedded-pgdata`; set empty for a temp DB)
### `schema-compat` (migration-compatibility probe)
Boots embedded Postgres and applies **Windmill's full migration set** on a single
connection, then samples core tables. This is the test that proved the schema is
Postgres-17 compatible (all migrations apply in ~1 s). It neutralizes the single
`uuid-ossp` `CREATE EXTENSION` line the same way Windmill's own migrator does.
```bash
cargo run --bin schema-compat
# -> [schema-compat] ALL 1182 migrations applied OK in ~950ms
```
+70
View File
@@ -0,0 +1,70 @@
//! Lightweight dev-mode launcher (WIN-2130 experiment).
//!
//! Boots an embedded PostgreSQL (via `pglite-oxide` / oliphaunt — Postgres
//! compiled to WASIX, no Docker, no local Postgres install), exports
//! `DATABASE_URL`, and execs the command passed after `--` with that env set.
//! The embedded server lives in this parent process and is shut down when the
//! child exits.
//!
//! Usage:
//! wm-embedded-db # just boot the DB and print the URL, then idle
//! wm-embedded-db -- ./target/debug/windmill # boot DB, then run windmill against it
//!
//! Env:
//! WM_PGDATA data dir (default: ./.wm-embedded-pgdata; empty => temporary)
use std::process::Command;
fn main() -> anyhow::Result<()> {
let mut args = std::env::args().skip(1);
let mut child_cmd: Vec<String> = Vec::new();
let mut seen_sep = false;
for a in args.by_ref() {
if !seen_sep && a == "--" {
seen_sep = true;
continue;
}
if seen_sep {
child_cmd.push(a);
}
}
let pgdata = std::env::var("WM_PGDATA").unwrap_or_else(|_| "./.wm-embedded-pgdata".to_string());
eprintln!("[wm-embedded-db] booting embedded Postgres (pglite-oxide)...");
let t0 = std::time::Instant::now();
let mut builder = pglite_oxide::PgliteServer::builder();
if pgdata.is_empty() {
builder = builder.temporary();
} else {
builder = builder.path(&pgdata);
}
let server = builder.start()?;
let url = server.database_url();
eprintln!(
"[wm-embedded-db] embedded Postgres ready in {:?}",
t0.elapsed()
);
eprintln!("[wm-embedded-db] DATABASE_URL={url}");
eprintln!(
"[wm-embedded-db] NOTE: pglite-oxide serves ONE backend connection at a time; \
point pools at a single connection."
);
if child_cmd.is_empty() {
eprintln!("[wm-embedded-db] no command given; idling. Ctrl-C to stop.");
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
}
eprintln!("[wm-embedded-db] exec: {}", child_cmd.join(" "));
let status = Command::new(&child_cmd[0])
.args(&child_cmd[1..])
.env("DATABASE_URL", &url)
.status()?;
eprintln!("[wm-embedded-db] child exited: {status}");
server.shutdown().ok();
std::process::exit(status.code().unwrap_or(1));
}
+101
View File
@@ -0,0 +1,101 @@
//! Schema-compatibility probe (WIN-2130).
//!
//! Boots embedded Postgres (pglite-oxide) and applies Windmill's full migration
//! set on a single connection, then samples core tables. Proves the schema is
//! Postgres-17 compatible independent of the runtime connection-concurrency
//! blocker documented in docs/experiments/oliphaunt-lightweight-dev-mode.md.
//!
//! Uses sqlx's runtime `Migrator::new(path)` (not the compile-time macro) so it
//! can read a patched copy of the migrations with the single `uuid-ossp`
//! CREATE EXTENSION line neutralized — the same thing Windmill's own migrator
//! does via OVERRIDDEN_MIGRATIONS (windmill-api/src/db.rs).
use pglite_oxide::PgliteServer;
use sqlx::migrate::Migrator;
use sqlx::postgres::PgPoolOptions;
use std::path::PathBuf;
use std::time::Duration;
const UUID_OSSP_LINE: &str =
"create extension if not exists \"uuid-ossp\" with schema extensions;";
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() -> anyhow::Result<()> {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let src = manifest.join("../../backend/migrations").canonicalize()?;
eprintln!("[schema-compat] source migrations: {}", src.display());
// Copy the (flat) migrations dir to a temp location and neutralize the one
// uuid-ossp line pglite can't satisfy.
let tmp = std::env::temp_dir().join("wm-embedded-db-migrations");
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp)?;
let mut patched = 0usize;
for entry in std::fs::read_dir(&src)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let name = entry.file_name();
let mut body = std::fs::read_to_string(entry.path())?;
if body.contains(UUID_OSSP_LINE) {
body = body.replace(
UUID_OSSP_LINE,
"-- uuid-ossp unavailable in pglite; neutralized (see OVERRIDDEN_MIGRATIONS)",
);
patched += 1;
}
std::fs::write(tmp.join(name), body)?;
}
eprintln!("[schema-compat] copied migrations to temp, patched {patched} file(s)");
let server = PgliteServer::builder()
.path(
manifest
.join(".schema-compat-pgdata")
.to_string_lossy()
.to_string(),
)
.start()?;
let url = server.database_url();
eprintln!("[schema-compat] embedded Postgres up: {url}");
let pool = PgPoolOptions::new()
.min_connections(1)
.max_connections(1)
.acquire_timeout(Duration::from_secs(120))
.connect(&url)
.await?;
let migrator = Migrator::new(tmp.as_path()).await?;
eprintln!(
"[schema-compat] applying {} migrations (single connection)...",
migrator.migrations.len()
);
let t0 = std::time::Instant::now();
match migrator.run(&pool).await {
Ok(()) => eprintln!(
"[schema-compat] ALL {} migrations applied OK in {:?}",
migrator.migrations.len(),
t0.elapsed()
),
Err(e) => {
eprintln!("[schema-compat] FAILED after {:?}: {e}", t0.elapsed());
pool.close().await;
server.shutdown().ok();
std::process::exit(1);
}
}
for t in ["workspace", "script", "flow", "v2_job", "usr"] {
let q = format!("select count(*) from {t}");
match sqlx::query_scalar::<_, i64>(&q).fetch_one(&pool).await {
Ok(c) => eprintln!("[schema-compat] table {t}: OK ({c} rows)"),
Err(e) => eprintln!("[schema-compat] table {t}: MISSING/ERR: {e}"),
}
}
pool.close().await;
server.shutdown().ok();
Ok(())
}