mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-15 00:02:56 +00:00
149 lines
5.7 KiB
Rust
149 lines
5.7 KiB
Rust
//! A fork's copy of a data table must report the migrations its schema already holds as applied.
|
|
//!
|
|
//! A schema-only copy restores `_wm_migrations` without its rows, and no copy carries the source's
|
|
//! `ALTER DATABASE ... SET search_path`, through which the bookkeeping table is found. Either one
|
|
//! alone makes the fork list every applied migration as pending, and running them fails on
|
|
//! objects that already exist. The source's `DateStyle` differs from the copy's while the rows
|
|
//! are written, so they must not travel in its text form.
|
|
|
|
use serde_json::{json, Value};
|
|
use sqlx::{Pool, Postgres};
|
|
|
|
use windmill_test_utils::*;
|
|
|
|
const ROLE: &str = "wm_dtfork_test_role";
|
|
const ROLE_PASSWORD: &str = "wm_dtfork_test_pwd";
|
|
|
|
fn authed(b: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
|
|
b.header("Authorization", "Bearer DTMIG_ADMIN_TOKEN")
|
|
}
|
|
|
|
#[sqlx::test(fixtures("datatable_migrations_grants"))]
|
|
async fn a_forked_copy_keeps_its_applied_migrations(db: Pool<Postgres>) -> anyhow::Result<()> {
|
|
initialize_tracing().await;
|
|
let opts = (*db.connect_options()).clone();
|
|
// The copy takes its settings locks on a connection of its own, opened from `DATABASE_URL`
|
|
// rather than the pool: point that at this test's database, not the one sqlx creates it from.
|
|
// Only this test runs in the binary, and before anything has read the variable.
|
|
let base_url = std::env::var("DATABASE_URL")?;
|
|
let (prefix, rest) = base_url
|
|
.rsplit_once('/')
|
|
.expect("DATABASE_URL names a database");
|
|
let query = rest
|
|
.split_once('?')
|
|
.map(|(_, q)| format!("?{q}"))
|
|
.unwrap_or_default();
|
|
let test_db = opts.get_database().expect("test database name");
|
|
std::env::set_var("DATABASE_URL", format!("{prefix}/{test_db}{query}"));
|
|
// Databases are cluster-wide, so both are named for this run alone.
|
|
let suffix = format!(
|
|
"{:x}",
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)?
|
|
.as_nanos()
|
|
);
|
|
let source = format!("wm_dtfork_src_{suffix}");
|
|
let fork_id = format!("wm-fork-dtfork-{suffix}");
|
|
let copy = format!("wm_fork_dtfork_{suffix}__main");
|
|
|
|
sqlx::query(&format!(
|
|
"DO $$ BEGIN \
|
|
CREATE ROLE {ROLE} LOGIN CREATEDB PASSWORD '{ROLE_PASSWORD}'; \
|
|
EXCEPTION WHEN duplicate_object OR unique_violation THEN NULL; \
|
|
END $$"
|
|
))
|
|
.execute(&db)
|
|
.await?;
|
|
sqlx::query(&format!("CREATE DATABASE {source} OWNER {ROLE}"))
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let result = async {
|
|
let source_pool = sqlx::PgPool::connect_with(opts.clone().database(&source)).await?;
|
|
sqlx::raw_sql(&format!(
|
|
"SET ROLE {ROLE}; \
|
|
CREATE SCHEMA app; \
|
|
CREATE TABLE app.foo (id int); \
|
|
CREATE TABLE app._wm_migrations (datatable TEXT NOT NULL, version BIGINT NOT NULL, \
|
|
installed_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (datatable, version)); \
|
|
INSERT INTO app._wm_migrations (datatable, version, installed_at) \
|
|
VALUES ('main', 1, '2026-01-31 10:00:00+00'); \
|
|
ALTER DATABASE {source} SET search_path = app; \
|
|
ALTER DATABASE {source} SET datestyle = 'SQL, DMY';"
|
|
))
|
|
.execute(&source_pool)
|
|
.await?;
|
|
source_pool.close().await;
|
|
|
|
sqlx::query(
|
|
"INSERT INTO resource (workspace_id, path, value, resource_type, created_by) \
|
|
VALUES ('dtmig-ws', 'u/dtmig-admin/pg', $1, 'postgresql', 'dtmig-admin')",
|
|
)
|
|
.bind(json!({
|
|
"host": opts.get_host(),
|
|
"port": opts.get_port(),
|
|
"dbname": source,
|
|
"user": ROLE,
|
|
"password": ROLE_PASSWORD,
|
|
"sslmode": "disable",
|
|
}))
|
|
.execute(&db)
|
|
.await?;
|
|
sqlx::query(
|
|
"INSERT INTO datatable_migrations (workspace_id, datatable, timestamp, name, code_up) \
|
|
VALUES ('dtmig-ws', 'main', 1, 'create_foo', 'CREATE TABLE foo (id int)')",
|
|
)
|
|
.execute(&db)
|
|
.await?;
|
|
|
|
let server = ApiServer::start(db.clone()).await?;
|
|
let base = format!("http://localhost:{}/api", server.addr.port());
|
|
let client = reqwest::Client::new();
|
|
|
|
let resp = authed(client.post(format!("{base}/w/dtmig-ws/workspaces/create_fork")))
|
|
.json(&json!({
|
|
"id": fork_id,
|
|
"name": "fork",
|
|
"forked_datatables": [
|
|
{"name": "main", "new_dbname": copy, "fork_behavior": "schema_only"}
|
|
],
|
|
}))
|
|
.send()
|
|
.await?;
|
|
// `ensure!` rather than `assert!`: a panic would skip dropping the databases below.
|
|
let fork_status = resp.status();
|
|
anyhow::ensure!(fork_status == 200, "{}", resp.text().await?);
|
|
|
|
let status: Value = authed(client.get(format!(
|
|
"{base}/w/{fork_id}/workspaces/datatable_migrations_status/main"
|
|
)))
|
|
.send()
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
anyhow::ensure!(status["migrations"][0]["status"] == "ran", "{status}");
|
|
|
|
let copied: Vec<String> = sqlx::query_scalar(
|
|
"SELECT unnest(setconfig) FROM pg_db_role_setting \
|
|
WHERE setrole = 0 AND setdatabase = (SELECT oid FROM pg_database WHERE datname = $1) \
|
|
ORDER BY 1",
|
|
)
|
|
.bind(©)
|
|
.fetch_all(&db)
|
|
.await?;
|
|
anyhow::ensure!(
|
|
copied == ["DateStyle=SQL, DMY", "search_path=app"],
|
|
"{copied:?}"
|
|
);
|
|
anyhow::Ok(())
|
|
}
|
|
.await;
|
|
|
|
for name in [©, &source] {
|
|
sqlx::query(&format!("DROP DATABASE IF EXISTS {name} WITH (FORCE)"))
|
|
.execute(&db)
|
|
.await?;
|
|
}
|
|
result
|
|
}
|