#[path = "../support/mod.rs"]
mod support;
use std::fs;
use serde_json::json;
use support::{Sandbox, local_d1, text};
const BATCH: &str = "cf d1 query uuid-shop --batch @.wrangler/ocre-batch.json";
fn write_seeds(root: &std::path::Path) {
fs::create_dir_all(root.join("db")).unwrap();
fs::write(root.join("db/seeds.sql"), "INSERT INTO posts (title) VALUES ('Hello');\n").unwrap();
}
fn batch(sql: &str) -> String {
format!("batch {}", json!([{ "sql": sql }]))
}
#[test]
fn migrate_status_lists_pending_migrations_locally_and_remotely() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.write_state("pending_migrations", "0001_create_posts.sql\n0002_add_slug.sql\n");
let output = sandbox.ocre(&["migrate", "--status"], &root);
let (stdout, _) = text(&output);
assert!(output.status.success());
assert_eq!(stdout, " pending 0001_create_posts.sql\n pending 0002_add_slug.sql\n\nNext:\n ocre migrate\n");
let derived: serde_json::Value =
serde_json::from_str(&fs::read_to_string(root.join(".wrangler/ocre-d1.json")).unwrap()).unwrap();
assert_eq!(
derived["d1_databases"],
json!([{ "binding": "DB", "database_name": "shop", "migrations_dir": "../migrations" }])
);
assert_eq!(derived["name"], "shop");
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["migrate", "--status", "--remote"], &root);
assert!(ok, "{report}");
assert_eq!(
report,
json!({
"ok": true,
"command": "migrate",
"pending": ["0001_create_posts.sql", "0002_add_slug.sql"],
"remote": true,
"next": ["ocre migrate --remote"],
})
);
assert_eq!(
sandbox.calls(),
[
local_d1("d1 migrations list DB --local"),
"cf d1 list --name shop".to_owned(),
"cf d1 migrations list uuid-shop".to_owned()
]
);
}
#[test]
fn migrate_status_with_nothing_pending_suggests_nothing() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["migrate", "--status"], &root);
assert!(ok, "{report}");
assert_eq!(report, json!({ "ok": true, "command": "migrate" }));
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["migrate", "--status", "--remote"], &root);
assert!(ok, "{report}");
assert_eq!(report, json!({ "ok": true, "command": "migrate", "remote": true }));
}
#[test]
fn migrate_status_failure_includes_the_tool_error() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.set("migrations_list_fails");
let (report, ok) = sandbox.json(&["migrate", "--status"], &root);
assert!(!ok);
let error = report["error"].as_str().unwrap();
assert!(error.starts_with("`wrangler d1 migrations list DB --local` failed (exit status: 1):\n"), "{error}");
assert!(error.contains("│ migrations_list_fails"), "{error}");
assert_eq!(report["hint"], "the wrangler output above names the cause");
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["migrate", "--status", "--remote"], &root);
assert!(!ok);
assert_eq!(report["error"], "`cf d1 migrations list uuid-shop` failed: ┌ Error\n│ migrations_list_fails\n└");
assert!(report["hint"].as_str().unwrap().contains("ocre login"), "{report}");
}
#[test]
fn remote_commands_need_the_database_on_cloudflare() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
write_seeds(&root);
sandbox.remote_database("shopping");
for args in
[&["migrate", "--status", "--remote"][..], &["db", "seed", "--remote"], &["sql", "SELECT 1", "--remote"]]
{
let (report, ok) = sandbox.json(args, &root);
assert!(!ok, "{args:?}: {report}");
assert_eq!(report["error"], "the D1 database shop does not exist on Cloudflare yet");
assert_eq!(report["hint"], "run `ocre deploy` (or `ocre db create --remote`) first");
}
assert_eq!(sandbox.calls(), ["cf d1 list --name shop"; 3]);
sandbox.set("d1_list_fails");
let (report, _) = sandbox.json(&["sql", "SELECT 1", "--remote"], &root);
assert_eq!(report["error"], "`cf d1 list --name shop` failed: ┌ Error\n│ d1_list_fails\n└");
}
#[test]
fn local_commands_need_the_apps_wrangler() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
fs::remove_dir_all(root.join("node_modules")).unwrap();
let (report, ok) = sandbox.json(&["sql", "SELECT 1"], &root);
assert!(!ok);
assert_eq!(report["error"], "the app's wrangler is not installed (node_modules/.bin/wrangler)");
assert!(report["hint"].as_str().unwrap().starts_with("run `npm install` in "), "{report}");
assert!(sandbox.calls().is_empty());
fs::create_dir_all(root.join("node_modules/.bin")).unwrap();
fs::write(root.join("node_modules/.bin/wrangler"), "not a program").unwrap();
let (report, ok) = sandbox.json(&["migrate"], &root);
assert!(!ok);
assert!(report["error"].as_str().unwrap().starts_with("could not run wrangler: "), "{report}");
assert!(report["hint"].as_str().unwrap().contains("`npm install`"), "{report}");
}
#[test]
fn db_seed_runs_the_seeds_file_locally_or_remotely() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
write_seeds(&root);
let output = sandbox.ocre(&["db", "seed"], &root.join("src"));
assert_eq!(text(&output).0, "Executed db/seeds.sql on DB (--local)\n loaded db/seeds.sql (--local)\n");
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["db", "seed", "--remote"], &root);
assert!(ok, "{report}");
assert_eq!(
report,
json!({ "ok": true, "command": "db seed", "ran": ["loaded db/seeds.sql (--remote)"], "remote": true })
);
assert_eq!(
sandbox.calls(),
[
local_d1("d1 execute DB --local --file db/seeds.sql --yes"),
"cf d1 list --name shop".to_owned(),
BATCH.to_owned(),
batch("INSERT INTO posts (title) VALUES ('Hello');\n"),
]
);
assert!(!root.join(".wrangler/ocre-batch.json").exists(), "the batch file is deleted");
let output = sandbox.ocre(&["db", "seed", "--remote"], &root);
assert!(text(&output).0.ends_with("Target: remote D1 database on Cloudflare\n"));
}
#[test]
fn db_seed_without_a_seeds_file_names_the_fix() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["db", "seed"], &root);
assert!(!ok);
assert_eq!(report["error"], "db/seeds.sql not found in the app");
assert_eq!(
report["hint"],
"create db/seeds.sql with INSERT statements or fixture files in db/fixtures/, then run `ocre db seed`"
);
assert!(sandbox.calls().is_empty());
}
#[test]
fn db_seed_failure_includes_the_tool_error() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
write_seeds(&root);
sandbox.set("execute_fails");
let (report, ok) = sandbox.json(&["db", "seed"], &root);
assert!(!ok);
assert!(
report["error"]
.as_str()
.unwrap()
.starts_with("`wrangler d1 execute DB --local --file db/seeds.sql --yes` failed")
);
sandbox.remote_database("shop");
sandbox.set("query_fails");
let (report, ok) = sandbox.json(&["db", "seed", "--remote"], &root);
assert!(!ok);
assert_eq!(report["error"], "`cf d1 query uuid-shop` failed: ┌ Error\n│ query_fails\n└");
assert!(!root.join(".wrangler/ocre-batch.json").exists(), "the batch file is deleted on failure too");
}
#[test]
fn db_reset_deletes_the_local_database_then_migrates_and_seeds() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
write_seeds(&root);
let state = root.join(".wrangler/state/v3/d1/miniflare-D1DatabaseObject");
fs::create_dir_all(&state).unwrap();
fs::write(state.join("db.sqlite"), "old").unwrap();
fs::write(root.join(".wrangler/state/v3/keep"), "other state").unwrap();
let (report, ok) = sandbox.json(&["db", "reset"], &root);
assert!(ok, "{report}");
assert_eq!(
report["ran"],
json!(["deleted .wrangler/state/v3/d1", "applied migrations (--local)", "loaded db/seeds.sql (--local)"])
);
assert!(report.get("remote").is_none());
assert!(!root.join(".wrangler/state/v3/d1").exists());
assert!(root.join(".wrangler/state/v3/keep").exists(), "only the D1 state is deleted");
assert_eq!(
sandbox.calls(),
[local_d1("d1 migrations apply DB --local"), local_d1("d1 execute DB --local --file db/seeds.sql --yes")]
);
}
#[test]
fn db_reset_without_local_state_or_seeds_only_migrates() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let output = sandbox.ocre(&["db", "reset"], &root);
assert_eq!(text(&output).0, "Migrations applied to DB (--local)\n applied migrations (--local)\n");
assert_eq!(sandbox.calls(), [local_d1("d1 migrations apply DB --local")]);
}
#[test]
fn db_reset_is_local_only() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let output = sandbox.ocre(&["db", "reset", "--remote"], &root);
assert!(!output.status.success());
assert!(text(&output).1.contains("unexpected argument '--remote'"));
assert!(sandbox.calls().is_empty());
}
const ROWS: &str =
r#"[{"results":[{"title":"Hello","id":1},{"title":null,"id":2}],"success":true,"meta":{"duration":0}}]"#;
#[test]
fn sql_prints_rows_as_a_table() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.write_state("execute.json", ROWS);
let output = sandbox.ocre(&["sql", "SELECT title, id FROM posts"], &root);
assert!(output.status.success());
assert_eq!(text(&output).0, "title | id\n------+---\nHello | 1\nNULL | 2\n(2 rows)\n");
assert_eq!(sandbox.calls(), [local_d1("d1 execute DB --local --command SELECT title, id FROM posts --json")]);
}
#[test]
fn sql_json_returns_the_results_and_flags_remote() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.remote_database("shop");
sandbox.write_state("execute.json", ROWS);
let (report, ok) = sandbox.json(&["sql", "SELECT 1", "--remote"], &root);
assert!(ok, "{report}");
assert_eq!(
report,
json!({
"ok": true,
"command": "sql",
"rows": serde_json::from_str::<serde_json::Value>(ROWS).unwrap(),
"remote": true,
})
);
assert_eq!(sandbox.calls(), ["cf d1 list --name shop".to_owned(), BATCH.to_owned(), batch("SELECT 1")]);
let output = sandbox.ocre(&["sql", "DELETE FROM posts", "--remote"], &root);
assert!(text(&output).0.ends_with("(2 rows)\nTarget: remote D1 database on Cloudflare\n"));
}
#[test]
fn sql_failure_includes_the_tool_error() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.set("execute_fails");
let (report, ok) = sandbox.json(&["sql", "SELECT nope"], &root);
assert!(!ok);
let error = report["error"].as_str().unwrap();
assert!(error.starts_with("`wrangler d1 execute DB --local --command SELECT nope --json` failed"), "{error}");
assert!(error.contains("│ execute_fails"), "{error}");
}
#[test]
fn sql_rejects_unexpected_output() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.write_state("execute.json", "Not JSON");
let (report, ok) = sandbox.json(&["sql", "SELECT 1"], &root);
assert!(!ok);
assert!(report["error"].as_str().unwrap().starts_with("unexpected D1 query output"));
}
const SCHEMA_ROWS: &str = r#"[{"results":[{"sql":"CREATE TABLE posts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT\n)"},{"sql":"CREATE INDEX index_posts_on_title ON posts (title)"},{"sql":null}],"success":true}]"#;
#[test]
fn db_schema_dumps_create_statements_for_rebuild_migrations() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
sandbox.write_state("execute.json", SCHEMA_ROWS);
let (report, ok) = sandbox.json(&["db", "schema"], &root);
assert!(ok, "{report}");
assert_eq!(report["created"], json!(["db/schema.sql"]));
let call = &sandbox.calls()[0];
assert!(
call.starts_with(
"wrangler d1 execute DB --local --command SELECT sql FROM sqlite_master WHERE sql IS NOT NULL"
),
"{call}"
);
assert!(call.ends_with("--json -c .wrangler/ocre-d1.json --persist-to .wrangler/state"), "{call}");
let schema = fs::read_to_string(root.join("db/schema.sql")).unwrap();
assert!(schema.starts_with("-- Schema of the local D1 database, written by `ocre db schema`"), "{schema}");
assert!(
schema.ends_with(
"\n\nCREATE TABLE posts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT\n);\n\n\
CREATE INDEX index_posts_on_title ON posts (title);\n"
),
"{schema}"
);
let (report, ok) = sandbox.json(&["g", "migration", "rebuild_posts"], &root);
assert!(ok, "{report}");
assert_eq!(report["next"], json!(["ocre migrate", "update the model in src/models/ to match the new columns"]));
let migration = fs::read_to_string(root.join("migrations/0001_rebuild_posts.sql")).unwrap();
assert!(migration.contains("INSERT INTO posts_new (id, title) SELECT id, title FROM posts;\n"), "{migration}");
assert!(migration.contains("CREATE INDEX index_posts_on_title ON posts (title);\n"), "{migration}");
sandbox.remote_database("shop");
let (report, ok) = sandbox.json(&["db", "schema", "--remote"], &root);
assert!(ok, "{report}");
assert_eq!((&report["updated"], &report["remote"]), (&json!(["db/schema.sql"]), &json!(true)));
let schema = fs::read_to_string(root.join("db/schema.sql")).unwrap();
assert!(schema.starts_with("-- Schema of the remote D1 database"), "{schema}");
sandbox.write_state("execute.json", "not json");
let (report, ok) = sandbox.json(&["db", "schema"], &root);
assert!(!ok);
assert!(report["error"].as_str().unwrap().starts_with("unexpected D1 query output"));
}
#[test]
fn migrations_rename_drop_and_index_from_their_name() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
let (report, ok) = sandbox.json(&["g", "migration", "add_index_to_posts", "author_id", "created_at"], &root);
assert!(ok, "{report}");
assert_eq!(report["next"], json!(["ocre migrate"]));
let sql = fs::read_to_string(root.join("migrations/0001_add_index_to_posts.sql")).unwrap();
assert!(sql.ends_with("CREATE INDEX index_posts_on_author_id_and_created_at ON posts (author_id, created_at);\n"));
let (report, _) = sandbox.json(&["g", "migration", "rebuild_posts"], &root);
assert_eq!(report["error"], "db/schema.sql not found");
let (report, ok) = sandbox.json(&["g", "migration", "rename_title_to_headline_in_posts"], &root);
assert!(ok, "{report}");
let sql = fs::read_to_string(root.join("migrations/0002_rename_title_to_headline_in_posts.sql")).unwrap();
assert!(sql.ends_with("ALTER TABLE posts RENAME COLUMN title TO headline;\n"), "{sql}");
}
#[test]
fn a_postgres_dump_becomes_a_migration_and_a_data_file_to_load() {
let sandbox = Sandbox::new();
let root = sandbox.new_app("shop", &[]);
fs::write(
root.join("dump.sql"),
"CREATE TABLE public.videos (\n id uuid NOT NULL,\n live boolean\n);\nCOPY public.videos (id, live) FROM stdin;\nabc\tt\n\\.\n",
)
.unwrap();
let (report, ok) = sandbox.json(&["db", "import-postgres", "dump.sql"], &root);
assert!(ok, "{report}");
assert_eq!(report["created"], json!(["migrations/0001_import_from_postgres.sql", "db/import_from_postgres.sql"]));
assert_eq!(report["ran"][0], "videos: 1 rows");
assert!(report["ran"][1].as_str().unwrap().starts_with("note: videos.id: uuid -> TEXT"), "{report}");
let data = fs::read_to_string(root.join("db/import_from_postgres.sql")).unwrap();
assert!(data.ends_with("INSERT INTO videos (id, live) VALUES\n('abc', 1);\n"), "{data}");
assert!(sandbox.calls().is_empty(), "nothing is loaded");
let (report, ok) = sandbox.json(&["db", "import-postgres", "dump.sql"], &root);
assert!(!ok);
assert_eq!(report["error"], "db/import_from_postgres.sql already exists");
let (report, _) = sandbox.json(&["db", "import-postgres", "missing.sql"], &root);
assert!(report["error"].as_str().unwrap().starts_with("cannot read missing.sql"), "{report}");
let (report, ok) = sandbox.json(&["db", "load", "db/import_from_postgres.sql"], &root);
assert!(ok, "{report}");
assert_eq!(report["ran"], json!(["loaded db/import_from_postgres.sql (--local)"]));
assert_eq!(sandbox.calls(), [local_d1("d1 execute DB --local --file db/import_from_postgres.sql --yes")]);
let (report, _) = sandbox.json(&["db", "load", "db/nope.sql"], &root);
assert_eq!(report["error"], "db/nope.sql not found");
}