Files
rdbms-playground/tests/it/blob_removal_migration.rs
T
claude@clouddev1 6b4c4dcea4
ci / gate (push) Failing after 35s
ci / manifests (push) Successful in 3s
website / deploy (push) Successful in 35s
feat(types)!: drop the blob column type (ADR-0005 Amendment 2)
blob was a dead-end: declarable but never fillable (no literal in either
mode, seed-unsupported), so a blob column could only ever hold NULL. Remove
Type::Blob + Value/CellValue::Blob + the base64 CSV path + the grammar slot
+ completion/render/type-change/seed handling + the binder refusal (whose
message also carried a user-facing "DSL" copy-rule bug). The vocabulary is
now nine types; base64 stays (clipboard OSC-52).

Backward compat is the ADR-0015 migration framework's first real use: a
v1->v2 format bump (CURRENT_SCHEMA_VERSION across serializer/parser/skeleton)
with a migrator that rewrites `type: blob` -> `type: text`, and a forced .db
rebuild from the migrated text when a blob column was actually converted
(the stale .db keeps a STRICT BLOB engine column). Conversion to text is
non-destructive and CSV-free. Covered by a full-stack integration test and a
Tier-4 PTY test that opens a real legacy v1-blob project.

Also sweeps the nine-type vocabulary through CLAUDE.md, requirements.md, the
website (type reference, seed doc, highlight grammar), and the ADR-0030/0033/
0035 cross-references; CHANGELOG Removed entry; handoff-79.

BREAKING CHANGE: the `blob` column type is removed. Existing projects that
declared a blob column are migrated on first open (the column becomes text;
the original project.yaml is kept as a .v1.bak).
2026-06-22 21:25:38 +00:00

108 lines
4.1 KiB
Rust

//! Full-stack test for **ADR-0005 Amendment 2** (drop `blob`): a saved
//! v1 project that declares a `blob` column is migrated to v2 (`blob` →
//! `text`) on open, and a rebuild from the migrated text reconstructs the
//! column as `text` with the row data preserved — the base64 cell survives
//! as a recoverable string. This exercises the real migrator + the
//! `rebuild_from_text` path the runtime forces when a blob column was
//! converted (the runtime loop itself isn't booted in integration tests).
use std::fs;
use rdbms_playground::db::Database;
use rdbms_playground::dsl::Type;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::persistence::migrations::{
MigratorRegistry, body_declares_blob_column, ensure_project_yaml_migrated,
};
fn rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("tokio rt")
}
#[test]
fn v1_blob_project_migrates_to_text_and_rebuilds() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path();
// A hand-written v1 project: a serial PK + a `blob` column carrying a
// (hand-edited, base64) value — the only way bytes ever reached a blob
// column, since no command can insert one.
let yaml = concat!(
"version: 1\n",
"project:\n created_at: 2026-01-01T00:00:00Z\n mode: simple\n",
"tables:\n",
" - name: Files\n",
" primary_key: [id]\n",
" columns:\n",
" - { name: id, type: serial }\n",
" - { name: data, type: blob }\n",
"relationships: []\n",
"indexes: []\n",
);
fs::write(root.join("project.yaml"), yaml).expect("write yaml");
fs::create_dir(root.join("data")).expect("data dir");
fs::write(root.join("data").join("Files.csv"), "id,data\n1,aGVsbG8=\n").expect("write csv");
// The runtime's blob-detection signal sees the column…
assert!(body_declares_blob_column(yaml), "blob column detected");
// 1. Migration runs (as the runtime does on open).
let outcome = ensure_project_yaml_migrated(root, &MigratorRegistry::production())
.expect("migration succeeds");
assert_eq!(outcome.migrated_from, Some(1), "v1 project was migrated");
let migrated = fs::read_to_string(root.join("project.yaml")).expect("read migrated");
assert!(migrated.contains("version: 2"), "version bumped: {migrated}");
assert!(
migrated.contains("{ name: data, type: text }"),
"blob column rewritten to text: {migrated}"
);
assert!(!migrated.contains("type: blob"), "no blob type remains: {migrated}");
// The pre-migration original is preserved as a .bak.
let bak = fs::read_to_string(root.join("project.yaml.v1.bak")).expect("read bak");
assert!(bak.contains("type: blob"), "bak keeps the original blob column");
// 2. Rebuild from the migrated text (as the runtime forces when a blob
// column was converted): the column is now `text` and the row data
// survives as a recoverable string.
let db = Database::open_with_persistence(
root.join("playground.db"),
Persistence::new(root.to_path_buf()),
)
.expect("open db");
rt().block_on(async {
db.rebuild_from_text(root.to_path_buf(), None)
.await
.expect("rebuild");
let desc = db
.describe_table("Files".to_string())
.await
.expect("describe");
let data_col = desc
.columns
.iter()
.find(|c| c.name == "data")
.expect("data column present");
assert_eq!(
data_col.user_type,
Some(Type::Text),
"the former blob column is now text",
);
let rows = db
.query_data("Files".to_string(), None, None)
.await
.expect("query");
assert_eq!(rows.rows.len(), 1, "row preserved through the migration");
assert_eq!(
rows.rows[0][1].as_deref(),
Some("aGVsbG8="),
"the base64 cell survives as recoverable text",
);
});
}