package com.ankurm.expandcontract; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.jdbc.core.simple.JdbcClient; import java.nio.file.Path; import java.sql.Connection; import java.sql.DriverManager; import static org.assertj.core.api.Assertions.assertThat; /** * Deploy 1 (EXPAND) in isolation: migrate to V2 and confirm two things a real rollout * depends on. First, that every pre-existing row got backfilled in the same migration. * Second - the part that is easy to get wrong - that Stage 1 code, completely unaware * the new column exists, still inserts rows exactly as it always has. If this second * assertion ever failed, the migration would not be additive and expand-contract would * not apply to it. See docs/02-the-expand-migration.md. */ class ExpandMigrationBackwardCompatibleTest { @Test void backfillsExistingRowsAndStaysCompatibleWithStage1Code(@TempDir Path tmp) throws Exception { Path db = tmp.resolve("expand"); Transcript t = Transcript.start("02-expand-backward-compatible", "Deploy 1 (EXPAND): additive column + backfill, Stage 1 code untouched"); // Seed one row the way Stage 1 always has, before the expand migration exists. TestSupport.migrateTo(db, "1"); JdbcClient preExpand = TestSupport.jdbcClient(db); preExpand.sql("INSERT INTO customers(name, email) VALUES (?, ?)") .param("Ada Lovelace").param("ada@example.test").update(); t.section("schema before Deploy 1"); try (Connection conn = DriverManager.getConnection(TestSupport.jdbcUrl(db), "sa", "")) { t.line(DbDump.table(conn, "select column_name from information_schema.columns " + "where table_name = 'CUSTOMERS' order by ordinal_position")); } // Deploy 1: the expand migration runs against the live database. No app restart. TestSupport.migrateTo(db, "2"); t.section("schema after Deploy 1 (email_address added)"); try (Connection conn = DriverManager.getConnection(TestSupport.jdbcUrl(db), "sa", "")) { t.line(DbDump.table(conn, "select column_name from information_schema.columns " + "where table_name = 'CUSTOMERS' order by ordinal_position")); t.section("Ada's row was backfilled by the migration itself"); String row = DbDump.table(conn, "select name, email, email_address from customers where name = 'Ada Lovelace'"); t.line(row); // Backfilled: both columns hold the same value for a row that predates dual-write code. assertThat(row).contains("ada@example.test"); assertThat(row.indexOf("ada@example.test")).isNotEqualTo(row.lastIndexOf("ada@example.test")); } // Stage 1 code has not been redeployed and does not know email_address exists. // Its insert statement is byte-for-byte what it was before Deploy 1. JdbcClient postExpand = TestSupport.jdbcClient(db); postExpand.sql("INSERT INTO customers(name, email) VALUES (?, ?)") .param("Grace Hopper").param("grace@example.test").update(); t.section("Stage 1's original INSERT still works, unmodified, after the migration"); try (Connection conn = DriverManager.getConnection(TestSupport.jdbcUrl(db), "sa", "")) { String row = DbDump.table(conn, "select name, email, email_address from customers where name = 'Grace Hopper'"); t.line(row); assertThat(row).contains("grace@example.test").contains("NULL"); } t.write(); } }