//! 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", ); }); }