{
  "id": "P174",
  "slug": "db-migrations",
  "key": "P174-db-migrations",
  "title": "DB migrations",
  "summary": "Four idempotent migration steps on a customers table: two orders end in different schemas, a code rollback leaves the schema behind, and the down steps run in reverse lose the phone column.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P174 - DB migrations\n\nA customers table and four migration steps, each idempotent and each with\na down step that undoes it: what their order and their rollbacks do.\n\n- `main.eml` - the menu, the table and the steps, two orders, the release\n  rolled back, the migrations rolled back, and an order of your own\n- `schema.eml` - the table as columns of values, each step up and down,\n  running a sequence forwards and its downs last first, and comparing\n  tables\n\nHow each part works:\n\n- Step 1 adds verified (no for everyone) if it is missing, step 2 sets it\n  for emails at shop, step 3 renames phone to mobile if phone exists, and\n  step 4 adds an empty phone if it is missing. Run twice in a row, each\n  step changes nothing the second time.\n- As in the corpus case `the-migration-was-idempotent-and-the-order-was-not`,\n  the same four steps end in different schemas: in the order 1234 the\n  rename comes first and step 4 adds a new, empty phone; in 1243 step 4\n  finds phone still there, and the rename then takes it.\n- As in `the-deploy-rolled-back-and-the-migration-did-not`, release 2 reads\n  mobile, and rolling the code back to release 1 leaves the schema as it\n  is: release 1 reads phone and finds 0 of its 3 numbers, which are in\n  mobile; after 1243 it would find no phone column at all.\n- As in `the-migration-steps-were-each-reversible-and-the-sequence-was-not`,\n  each down step undoes its own up step, and the downs run last first take\n  1234 back exactly where it started; after 1243 they lose the phone\n  column, because step 4 did nothing on the way up and its down drops the\n  original phone, renamed back.\n- For an order of your own: in some orders the sequence run twice changes\n  again, though every step is idempotent.\n\nWhat is checked: menu choices 1 to 6; an order of the digits 1 to 4, each\nonce. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the table and the steps, the two orders, the release rolled back and the\n  migrations rolled back;\n- orders of your own: 2413, 4321 and 1234.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- orders: a cancel, 123, 12345, 1123, 1205, abcd and 1 2 3;\n- the order 3412.\n\nBuilt on the verified corpus cases `the-migration-was-idempotent-and-the-order-was-not`\n(twelve idempotent steps replayed onto three shards),\n`the-deploy-rolled-back-and-the-migration-did-not` (a rollback that leaves\nthe migrated table in place) and\n`the-migration-steps-were-each-reversible-and-the-sequence-was-not` (down\nmigrations, each correct alone, run as a sequence).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P174 db migrations: a customers table and four idempotent steps - the\n# table and the steps, two orders, the release rolled back, the migrations\n# rolled back, and an order of your own.\nimport schema\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    s => out\n    while len(out) < n:\n        out + \" \" => out\n    return out\n\ndef joined(xs):\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \", \" => out\n        out + x => out\n    return out\n\ndef names(t):\n    [] => out\n    for c in t:\n        out + [c[0]] => out\n    return out\n\ndef show_table(t):\n    for c in t:\n        [] => vs\n        for v in c[1]:\n            if v == \"\":\n                vs + [\"-\"] => vs\n            else:\n                vs + [v] => vs\n        (\"  \" + pad(c[0], 10) + joined(vs)) ^0\n\ndef label(order):\n    \"\" => s\n    for k in order:\n        s + str(k) => s\n    return s\n\ndef show_start():\n    \"The customers table (- is empty):\" ^0\n    show_table(schema.start())\n    \"The migration steps, each with a down step that undoes it:\" ^0\n    for s in schema.steps():\n        (\"  \" + str(s[0]) + \"  up: \" + pad(s[1], 46) + \"down: \" + s[2]) ^0\n    \"Each step is idempotent: run twice in a row, the second run changes nothing.\" ^0\n\ndef show_orders():\n    schema.start() => t0\n    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:\n        schema.run_up(t0, order) => t\n        (\"In the order \" + label(order) + \": \" + joined(names(t)) + \".\") ^0\n        show_table(t)\n    \"Both orders run the same four idempotent steps. In 1234 the rename comes first and step 4 adds a new, empty phone; in 1243 step 4 finds phone still there and does nothing, and then the rename takes it.\" ^0\n    \"Idempotence says what running a step again does. It says nothing about the order.\" ^0\n\ndef show_release():\n    schema.run_up(schema.start(), [1, 2, 3, 4]) => t\n    \"Release 2 reads mobile; it was deployed with the steps run in the order 1234. Release 2 is then rolled back to release 1, which reads phone. The rollback takes 90 seconds, and the schema stays as it is:\" ^0\n    schema.find(t, \"phone\") => p\n    0 => had\n    for v in schema.start()[schema.find(schema.start(), \"phone\")][1]:\n        if v != \"\":\n            had + 1 => had\n    0 => shows\n    for v in t[p][1]:\n        if v != \"\":\n            shows + 1 => shows\n    (\"  release 1 reads phone and finds \" + str(shows) + \" numbers, where it had \" + str(had) + \": they are in mobile, which release 1 does not know.\") ^0\n    \"  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read.\" ^0\n    \"Rolling back the code is not rolling back the data.\" ^0\n\ndef compare(a, b):\n    # what b lost and changed, against a\n    [] => lost\n    [] => changed\n    for c in a:\n        schema.find(b, c[0]) => i\n        if i < 0:\n            lost + [c[0]] => lost\n        elif b[i][1] != c[1]:\n            changed + [c[0]] => changed\n    [] => extra\n    for c in b:\n        if schema.find(a, c[0]) < 0:\n            extra + [c[0]] => extra\n    return [lost, changed, extra]\n\ndef describe_down(order):\n    schema.start() => t0\n    schema.run_down(schema.run_up(t0, order), order) => back\n    compare(t0, back) => d\n    if len(d[0]) == 0 and len(d[1]) == 0 and len(d[2]) == 0:\n        return \"back exactly where it started.\"\n    \"\" => s\n    if len(d[0]) > 0:\n        s + \"lost \" + joined(d[0]) => s\n    if len(d[1]) > 0:\n        if s != \"\":\n            s + \"; \" => s\n        s + \"changed \" + joined(d[1]) => s\n    if len(d[2]) > 0:\n        if s != \"\":\n            s + \"; \" => s\n        s + \"left behind \" + joined(d[2]) => s\n    return \"not where it started: \" + s + \".\"\n\ndef show_down():\n    \"Each down step undoes its own up step. The downs, run last first:\" ^0\n    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:\n        (\"  ups in the order \" + label(order) + \", downs \" + label([order[3], order[2], order[1], order[0]]) + \": \" + describe_down(order)) ^0\n    \"In 1243 step 4 did nothing on the way up, and its down drops phone anyway: by then phone is the original column, renamed back, with its 3 numbers.\" ^0\n\ndef own():\n    plain(input(\"an order of the four steps, e.g. 2413> \")) => t\n    if t == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => order\n    True => ok\n    if len(t) != 4:\n        False => ok\n    for c in t:\n        if c < \"1\" or c > \"4\":\n            False => ok\n        elif ok:\n            int(c) => k\n            for x in order:\n                if x == k:\n                    False => ok\n            order + [k] => order\n    if not ok or len(order) != 4:\n        \"Type the digits 1 to 4, each once.\" ^0\n        return\n    schema.start() => t0\n    schema.run_up(t0, order) => t1\n    (\"In the order \" + t + \": \" + joined(names(t1)) + \".\") ^0\n    show_table(t1)\n    if schema.same(schema.run_up(t1, order), t1):\n        \"Run again in the same order, nothing changes.\" ^0\n    else:\n        \"Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.\" ^0\n    (\"Rolled back, last first: \" + describe_down(order)) ^0\n\n\"== DB migrations ==\" ^0\n\"Four idempotent migration steps on a customers table, and what their order and their rollbacks do.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_start()\n    elif choice == \"2\":\n        show_orders()\n    elif choice == \"3\":\n        show_release()\n    elif choice == \"4\":\n        show_down()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import schema\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    out = s\n    while len(out) < n:\n        out = out + \" \"\n    return out\n\ndef joined(xs):\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \", \"\n        out = out + x\n    return out\n\ndef names(t):\n    out = []\n    for c in t:\n        out = out + [c[0]]\n    return out\n\ndef show_table(t):\n    for c in t:\n        vs = []\n        for v in c[1]:\n            if v == \"\":\n                vs = vs + [\"-\"]\n            else:\n                vs = vs + [v]\n        print(\"  \" + pad(c[0], 10) + joined(vs))\n\ndef label(order):\n    s = \"\"\n    for k in order:\n        s = s + str(k)\n    return s\n\ndef show_start():\n    print(\"The customers table (- is empty):\")\n    show_table(schema.start())\n    print(\"The migration steps, each with a down step that undoes it:\")\n    for s in schema.steps():\n        print(\"  \" + str(s[0]) + \"  up: \" + pad(s[1], 46) + \"down: \" + s[2])\n    print(\"Each step is idempotent: run twice in a row, the second run changes nothing.\")\n\ndef show_orders():\n    t0 = schema.start()\n    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:\n        t = schema.run_up(t0, order)\n        print(\"In the order \" + label(order) + \": \" + joined(names(t)) + \".\")\n        show_table(t)\n    print(\"Both orders run the same four idempotent steps. In 1234 the rename comes first and step 4 adds a new, empty phone; in 1243 step 4 finds phone still there and does nothing, and then the rename takes it.\")\n    print(\"Idempotence says what running a step again does. It says nothing about the order.\")\n\ndef show_release():\n    t = schema.run_up(schema.start(), [1, 2, 3, 4])\n    print(\"Release 2 reads mobile; it was deployed with the steps run in the order 1234. Release 2 is then rolled back to release 1, which reads phone. The rollback takes 90 seconds, and the schema stays as it is:\")\n    p = schema.find(t, \"phone\")\n    had = 0\n    for v in schema.start()[schema.find(schema.start(), \"phone\")][1]:\n        if v != \"\":\n            had = had + 1\n    shows = 0\n    for v in t[p][1]:\n        if v != \"\":\n            shows = shows + 1\n    print(\"  release 1 reads phone and finds \" + str(shows) + \" numbers, where it had \" + str(had) + \": they are in mobile, which release 1 does not know.\")\n    print(\"  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read.\")\n    print(\"Rolling back the code is not rolling back the data.\")\n\ndef compare(a, b):\n    lost = []\n    changed = []\n    for c in a:\n        i = schema.find(b, c[0])\n        if i < 0:\n            lost = lost + [c[0]]\n        elif b[i][1] != c[1]:\n            changed = changed + [c[0]]\n    extra = []\n    for c in b:\n        if schema.find(a, c[0]) < 0:\n            extra = extra + [c[0]]\n    return [lost, changed, extra]\n\ndef describe_down(order):\n    t0 = schema.start()\n    back = schema.run_down(schema.run_up(t0, order), order)\n    d = compare(t0, back)\n    if len(d[0]) == 0 and len(d[1]) == 0 and len(d[2]) == 0:\n        return \"back exactly where it started.\"\n    s = \"\"\n    if len(d[0]) > 0:\n        s = s + \"lost \" + joined(d[0])\n    if len(d[1]) > 0:\n        if s != \"\":\n            s = s + \"; \"\n        s = s + \"changed \" + joined(d[1])\n    if len(d[2]) > 0:\n        if s != \"\":\n            s = s + \"; \"\n        s = s + \"left behind \" + joined(d[2])\n    return \"not where it started: \" + s + \".\"\n\ndef show_down():\n    print(\"Each down step undoes its own up step. The downs, run last first:\")\n    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:\n        print(\"  ups in the order \" + label(order) + \", downs \" + label([order[3], order[2], order[1], order[0]]) + \": \" + describe_down(order))\n    print(\"In 1243 step 4 did nothing on the way up, and its down drops phone anyway: by then phone is the original column, renamed back, with its 3 numbers.\")\n\ndef own():\n    t = plain(input(\"an order of the four steps, e.g. 2413> \"))\n    if t == \"\":\n        print(\"Cancelled.\")\n        return\n    order = []\n    ok = True\n    if len(t) != 4:\n        ok = False\n    for c in t:\n        if c < \"1\" or c > \"4\":\n            ok = False\n        elif ok:\n            k = int(c)\n            for x in order:\n                if x == k:\n                    ok = False\n            order = order + [k]\n    if not ok or len(order) != 4:\n        print(\"Type the digits 1 to 4, each once.\")\n        return\n    t0 = schema.start()\n    t1 = schema.run_up(t0, order)\n    print(\"In the order \" + t + \": \" + joined(names(t1)) + \".\")\n    show_table(t1)\n    if schema.same(schema.run_up(t1, order), t1):\n        print(\"Run again in the same order, nothing changes.\")\n    else:\n        print(\"Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.\")\n    print(\"Rolled back, last first: \" + describe_down(order))\n\nprint(\"== DB migrations ==\")\nprint(\"Four idempotent migration steps on a customers table, and what their order and their rollbacks do.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_start()\n    elif choice == \"2\":\n        show_orders()\n    elif choice == \"3\":\n        show_release()\n    elif choice == \"4\":\n        show_down()\n    elif choice == \"5\":\n        own()\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "schema.eml",
      "eml": "# P174 db migrations - a customers table and four migration steps, each one\n# idempotent and each with a down step that undoes it. As in the corpus case\n# the-migration-was-idempotent-and-the-order-was-not, running every step\n# twice changes nothing, and running them in another order ends somewhere\n# else. As in the-deploy-rolled-back-and-the-migration-did-not, rolling the\n# code back leaves the schema where the migration put it. And as in\n# the-migration-steps-were-each-reversible-and-the-sequence-was-not, each down\n# step undoes its own up step, and the downs run in reverse do not undo the\n# sequence. A table is a list of [column, values].\n\ndef start():\n    return [[\"id\", [\"c1\", \"c2\", \"c3\", \"c4\"]],\n            [\"name\", [\"Ana\", \"Ben\", \"Cleo\", \"Dan\"]],\n            [\"phone\", [\"555-0101\", \"555-0102\", \"\", \"555-0104\"]],\n            [\"email\", [\"ana@shop\", \"ben@mail\", \"cleo@shop\", \"dan@mail\"]]]\n\ndef steps():\n    # [number, what the up step does, what its down step does]\n    return [[1, \"add verified, false for everyone, if missing\", \"drop verified\"],\n            [2, \"set verified for emails at shop\", \"clear verified\"],\n            [3, \"rename phone to mobile, if phone exists\", \"rename mobile to phone\"],\n            [4, \"add phone, empty, if missing\", \"drop phone\"]]\n\ndef copy(t):\n    [] => out\n    for c in t:\n        [] => vs\n        for v in c[1]:\n            vs + [v] => vs\n        out + [[c[0], vs]] => out\n    return out\n\ndef find(t, name):\n    0 => i\n    while i < len(t):\n        if t[i][0] == name:\n            return i\n        i + 1 => i\n    return 0 - 1\n\ndef blank(n, v):\n    [] => out\n    while len(out) < n:\n        out + [v] => out\n    return out\n\ndef drop(t, name):\n    [] => out\n    for c in t:\n        if c[0] != name:\n            out + [c] => out\n    return out\n\ndef ends_with(s, tail):\n    if len(s) < len(tail):\n        return False\n    return s[len(s) - len(tail):] == tail\n\ndef up(t, k):\n    # step k applied to table t: [new table, whether it changed anything]\n    copy(t) => t\n    len(t[0][1]) => n\n    if k == 1:\n        if find(t, \"verified\") >= 0:\n            return [t, False]\n        return [t + [[\"verified\", blank(n, \"no\")]], True]\n    if k == 2:\n        find(t, \"verified\") => v\n        find(t, \"email\") => e\n        if v < 0:\n            return [t, False]\n        False => changed\n        0 => i\n        while i < n:\n            if ends_with(t[e][1][i], \"@shop\") and t[v][1][i] != \"yes\":\n                \"yes\" => t[v][1][i]\n                True => changed\n            i + 1 => i\n        return [t, changed]\n    if k == 3:\n        find(t, \"phone\") => p\n        if p < 0 or find(t, \"mobile\") >= 0:\n            return [t, False]\n        \"mobile\" => t[p][0]\n        return [t, True]\n    if find(t, \"phone\") >= 0:\n        return [t, False]\n    return [t + [[\"phone\", blank(n, \"\")]], True]\n\ndef down(t, k):\n    # step k's down step applied to table t\n    copy(t) => t\n    if k == 1:\n        return drop(t, \"verified\")\n    if k == 2:\n        find(t, \"verified\") => v\n        if v >= 0:\n            blank(len(t[0][1]), \"no\") => t[v][1]\n        return t\n    if k == 3:\n        find(t, \"mobile\") => m\n        if m >= 0 and find(t, \"phone\") < 0:\n            \"phone\" => t[m][0]\n        return t\n    return drop(t, \"phone\")\n\ndef run_up(t, order):\n    for k in order:\n        up(t, k)[0] => t\n    return t\n\ndef run_down(t, order):\n    # the down steps of order, last first\n    len(order) - 1 => i\n    while i >= 0:\n        down(t, order[i]) => t\n        i - 1 => i\n    return t\n\ndef same(a, b):\n    if len(a) != len(b):\n        return False\n    0 => i\n    while i < len(a):\n        if a[i][0] != b[i][0] or a[i][1] != b[i][1]:\n            return False\n        i + 1 => i\n    return True\n",
      "python": "def start():\n    return [[\"id\", [\"c1\", \"c2\", \"c3\", \"c4\"]], [\"name\", [\"Ana\", \"Ben\", \"Cleo\", \"Dan\"]], [\"phone\", [\"555-0101\", \"555-0102\", \"\", \"555-0104\"]], [\"email\", [\"ana@shop\", \"ben@mail\", \"cleo@shop\", \"dan@mail\"]]]\n\ndef steps():\n    return [[1, \"add verified, false for everyone, if missing\", \"drop verified\"], [2, \"set verified for emails at shop\", \"clear verified\"], [3, \"rename phone to mobile, if phone exists\", \"rename mobile to phone\"], [4, \"add phone, empty, if missing\", \"drop phone\"]]\n\ndef copy(t):\n    out = []\n    for c in t:\n        vs = []\n        for v in c[1]:\n            vs = vs + [v]\n        out = out + [[c[0], vs]]\n    return out\n\ndef find(t, name):\n    i = 0\n    while i < len(t):\n        if t[i][0] == name:\n            return i\n        i = i + 1\n    return 0 - 1\n\ndef blank(n, v):\n    out = []\n    while len(out) < n:\n        out = out + [v]\n    return out\n\ndef drop(t, name):\n    out = []\n    for c in t:\n        if c[0] != name:\n            out = out + [c]\n    return out\n\ndef ends_with(s, tail):\n    if len(s) < len(tail):\n        return False\n    return s[len(s) - len(tail):] == tail\n\ndef up(t, k):\n    t = copy(t)\n    n = len(t[0][1])\n    if k == 1:\n        if find(t, \"verified\") >= 0:\n            return [t, False]\n        return [t + [[\"verified\", blank(n, \"no\")]], True]\n    if k == 2:\n        v = find(t, \"verified\")\n        e = find(t, \"email\")\n        if v < 0:\n            return [t, False]\n        changed = False\n        i = 0\n        while i < n:\n            if ends_with(t[e][1][i], \"@shop\") and t[v][1][i] != \"yes\":\n                t[v][1][i] = \"yes\"\n                changed = True\n            i = i + 1\n        return [t, changed]\n    if k == 3:\n        p = find(t, \"phone\")\n        if p < 0 or find(t, \"mobile\") >= 0:\n            return [t, False]\n        t[p][0] = \"mobile\"\n        return [t, True]\n    if find(t, \"phone\") >= 0:\n        return [t, False]\n    return [t + [[\"phone\", blank(n, \"\")]], True]\n\ndef down(t, k):\n    t = copy(t)\n    if k == 1:\n        return drop(t, \"verified\")\n    if k == 2:\n        v = find(t, \"verified\")\n        if v >= 0:\n            t[v][1] = blank(len(t[0][1]), \"no\")\n        return t\n    if k == 3:\n        m = find(t, \"mobile\")\n        if m >= 0 and find(t, \"phone\") < 0:\n            t[m][0] = \"phone\"\n        return t\n    return drop(t, \"phone\")\n\ndef run_up(t, order):\n    for k in order:\n        t = up(t, k)[0]\n    return t\n\ndef run_down(t, order):\n    i = len(order) - 1\n    while i >= 0:\n        t = down(t, order[i])\n        i = i - 1\n    return t\n\ndef same(a, b):\n    if len(a) != len(b):\n        return False\n    i = 0\n    while i < len(a):\n        if a[i][0] != b[i][0] or a[i][1] != b[i][1]:\n            return False\n        i = i + 1\n    return True\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n5\n\n5\n123\n5\n12345\n5\n1123\n5\n1205\n5\nabcd\n5\n1 2 3\n5\n3412\n6\n",
      "screen": "== DB migrations ==\nFour idempotent migration steps on a customers table, and what their order and their rollbacks do.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> \nCancelled.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 123\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 12345\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 1123\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 1205\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> abcd\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 1 2 3\nType the digits 1 to 4, each once.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 3412\nIn the order 3412: id, name, mobile, email, phone, verified.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  phone     -, -, -, -\n  verified  yes, no, yes, no\nRun again in the same order, nothing changes.\nRolled back, last first: back exactly where it started.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\n4\n5\n2413\n5\n4321\n5\n1234\n6\n",
      "screen": "== DB migrations ==\nFour idempotent migration steps on a customers table, and what their order and their rollbacks do.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 1\nThe customers table (- is empty):\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  phone     555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\nThe migration steps, each with a down step that undoes it:\n  1  up: add verified, false for everyone, if missing  down: drop verified\n  2  up: set verified for emails at shop               down: clear verified\n  3  up: rename phone to mobile, if phone exists       down: rename mobile to phone\n  4  up: add phone, empty, if missing                  down: drop phone\nEach step is idempotent: run twice in a row, the second run changes nothing.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 2\nIn the order 1234: id, name, mobile, email, verified, phone.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  verified  yes, no, yes, no\n  phone     -, -, -, -\nIn the order 1243: id, name, mobile, email, verified.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  verified  yes, no, yes, no\nBoth orders run the same four idempotent steps. In 1234 the rename comes first and step 4 adds a new, empty phone; in 1243 step 4 finds phone still there and does nothing, and then the rename takes it.\nIdempotence says what running a step again does. It says nothing about the order.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 3\nRelease 2 reads mobile; it was deployed with the steps run in the order 1234. Release 2 is then rolled back to release 1, which reads phone. The rollback takes 90 seconds, and the schema stays as it is:\n  release 1 reads phone and finds 0 numbers, where it had 3: they are in mobile, which release 1 does not know.\n  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read.\nRolling back the code is not rolling back the data.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 4\nEach down step undoes its own up step. The downs, run last first:\n  ups in the order 1234, downs 4321: back exactly where it started.\n  ups in the order 1243, downs 3421: not where it started: lost phone.\nIn 1243 step 4 did nothing on the way up, and its down drops phone anyway: by then phone is the original column, renamed back, with its 3 numbers.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 2413\nIn the order 2413: id, name, mobile, email, verified.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  verified  no, no, no, no\nRun again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.\nRolled back, last first: not where it started: lost phone.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 4321\nIn the order 4321: id, name, mobile, email, verified.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  verified  no, no, no, no\nRun again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.\nRolled back, last first: not where it started: lost phone.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 5\nan order of the four steps, e.g. 2413> 1234\nIn the order 1234: id, name, mobile, email, verified, phone.\n  id        c1, c2, c3, c4\n  name      Ana, Ben, Cleo, Dan\n  mobile    555-0101, 555-0102, -, 555-0104\n  email     ana@shop, ben@mail, cleo@shop, dan@mail\n  verified  yes, no, yes, no\n  phone     -, -, -, -\nRun again in the same order, nothing changes.\nRolled back, last first: back exactly where it started.\n\n1) the table and the steps  2) two orders  3) the release rolled back  4) the migrations rolled back  5) an order of your own  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "the-migration-was-idempotent-and-the-order-was-not",
      "caseId": "603-the-migration-was-idempotent-and-the-order-was-not",
      "title": "The migration was idempotent and the order was not"
    },
    {
      "slug": "the-deploy-rolled-back-and-the-migration-did-not",
      "caseId": "634-the-deploy-rolled-back-and-the-migration-did-not",
      "title": "The deploy rolled back and the migration did not"
    },
    {
      "slug": "the-migration-steps-were-each-reversible-and-the-sequence-was-not",
      "caseId": "812-the-migration-steps-were-each-reversible-and-the-sequence-was-not",
      "title": "The migration steps were each reversible and the sequence was not"
    }
  ],
  "updated": "2026-10-19"
}
