package com.ankurm.expandcontract; import com.ankurm.expandcontract.customer.CustomerService; 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 static org.assertj.core.api.Assertions.assertThat; /** * A trap this article's own first draft of the V2 migration fell into: adding * {@code email_address} is not the whole story of "expand" when the column being * retired is {@code NOT NULL}. Stage 4 code never writes "email" - so if "email" is * still mandatory when Stage 4 starts, every create fails, in production, the moment * that deploy reaches its first replica. The fix is one more line in the SAME * migration: {@code ALTER TABLE customers ALTER COLUMN email DROP NOT NULL}. See * docs/06-the-not-null-trap.md, and compare * {@link com.ankurm.expandcontract.customer.CustomerService#create} - the shipped * V2 migration (with the fix) is what every other test in this module runs against. */ class NotNullConstraintTrapTest { @Test void stage4FailsIfTheOldColumnIsStillMandatory(@TempDir Path tmp) throws Exception { Path db = tmp.resolve("not-null-trap"); TestSupport.migrateTo(db, "1"); JdbcClient jdbc = TestSupport.jdbcClient(db); Transcript t = Transcript.start("06-not-null-trap", "The NOT NULL trap: expand without relaxing the old column's constraint"); // The NAIVE version of V2: add the column, backfill, stop there. This is // exactly V2 minus its last line. jdbc.sql("ALTER TABLE customers ADD COLUMN email_address VARCHAR(320)").update(); jdbc.sql("UPDATE customers SET email_address = email WHERE email_address IS NULL").update(); CustomerService stage4 = new CustomerService(jdbc, 4); Throwable thrown = null; try { stage4.create("Too Early", "too.early@example.test"); } catch (Throwable ex) { thrown = ex; } t.section("Stage 4 create() against the NAIVE migration (no DROP NOT NULL)"); assertThat(thrown).isNotNull(); Throwable root = thrown; while (root.getCause() != null) { root = root.getCause(); } t.line(thrown.getClass().getName() + ": " + thrown.getMessage()); t.line("root cause: " + root.getClass().getName() + ": " + root.getMessage()); assertThat(root.getMessage()).contains("NULL not allowed for column \"EMAIL\""); t.section("Stage 4 create() against the SHIPPED V2 migration (DROP NOT NULL included)"); Path fixedDb = tmp.resolve("not-null-fixed"); TestSupport.migrateTo(fixedDb, "2"); CustomerService fixedStage4 = new CustomerService(TestSupport.jdbcClient(fixedDb), 4); long id = fixedStage4.create("On Time", "on.time@example.test"); var result = fixedStage4.findById(id).orElseThrow(); t.line(result.toString()); assertThat(result.email()).isEqualTo("on.time@example.test"); t.write(); } }