Project P174

DB migrations

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.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-19

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

A customers table and four migration steps, each idempotent and each with a down step that undoes it: what their order and their rollbacks do.

  • main.eml - the menu, the table and the steps, two orders, the release rolled back, the migrations rolled back, and an order of your own
  • schema.eml - the table as columns of values, each step up and down, running a sequence forwards and its downs last first, and comparing tables

How each part works:

  • Step 1 adds verified (no for everyone) if it is missing, step 2 sets it for emails at shop, step 3 renames phone to mobile if phone exists, and step 4 adds an empty phone if it is missing. Run twice in a row, each step changes nothing the second time.
  • As in the corpus case the-migration-was-idempotent-and-the-order-was-not, the same four steps end in different schemas: in the order 1234 the rename comes first and step 4 adds a new, empty phone; in 1243 step 4 finds phone still there, and the rename then takes it.
  • As in the-deploy-rolled-back-and-the-migration-did-not, release 2 reads mobile, and rolling the code back to release 1 leaves the schema as it is: release 1 reads phone and finds 0 of its 3 numbers, which are in mobile; after 1243 it would find no phone column at all.
  • As in the-migration-steps-were-each-reversible-and-the-sequence-was-not, each down step undoes its own up step, and the downs run last first take 1234 back exactly where it started; after 1243 they lose the phone column, because step 4 did nothing on the way up and its down drops the original phone, renamed back.
  • For an order of your own: in some orders the sequence run twice changes again, though every step is idempotent.

What is checked: menu choices 1 to 6; an order of the digits 1 to 4, each once. An empty answer cancels.

Sessions: sessions/basic.in gives: - the table and the steps, the two orders, the release rolled back and the migrations rolled back; - orders of your own: 2413, 4321 and 1234.

sessions/bad-input.in gives: - menu choices 0 and x; - orders: a cancel, 123, 12345, 1123, 1205, abcd and 1 2 3; - the order 3412.

Built on the verified corpus cases the-migration-was-idempotent-and-the-order-was-not (twelve idempotent steps replayed onto three shards), the-deploy-rolled-back-and-the-migration-did-not (a rollback that leaves the migrated table in place) and the-migration-steps-were-each-reversible-and-the-sequence-was-not (down migrations, each correct alone, run as a sequence).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== DB migrations ==
Four idempotent migration steps on a customers table, and what their order and their rollbacks do.

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
choice> 0
Pick a number from 1 to 6.

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
choice> x
Pick a number from 1 to 6.

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
choice> 5
an order of the four steps, e.g. 2413> 
Cancelled.

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
choice> 5
an order of the four steps, e.g. 2413> 123
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> 12345
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> 1123
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> 1205
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> abcd
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> 1 2 3
Type the digits 1 to 4, each once.

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
choice> 5
an order of the four steps, e.g. 2413> 3412
In the order 3412: id, name, mobile, email, phone, verified.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  phone     -, -, -, -
  verified  yes, no, yes, no
Run again in the same order, nothing changes.
Rolled back, last first: back exactly where it started.

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
choice> 6
Bye.
What was typed (19 lines)
0
x
5

5
123
5
12345
5
1123
5
1205
5
abcd
5
1 2 3
5
3412
6

basic

interpreter: byte-equal
== DB migrations ==
Four idempotent migration steps on a customers table, and what their order and their rollbacks do.

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
choice> 1
The customers table (- is empty):
  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
The migration steps, each with a down step that undoes it:
  1  up: add verified, false for everyone, if missing  down: drop verified
  2  up: set verified for emails at shop               down: clear verified
  3  up: rename phone to mobile, if phone exists       down: rename mobile to phone
  4  up: add phone, empty, if missing                  down: drop phone
Each step is idempotent: run twice in a row, the second run changes nothing.

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
choice> 2
In the order 1234: id, name, mobile, email, verified, phone.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  verified  yes, no, yes, no
  phone     -, -, -, -
In the order 1243: id, name, mobile, email, verified.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  verified  yes, no, yes, no
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.
Idempotence says what running a step again does. It says nothing about the order.

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
choice> 3
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:
  release 1 reads phone and finds 0 numbers, where it had 3: they are in mobile, which release 1 does not know.
  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read.
Rolling back the code is not rolling back the data.

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
choice> 4
Each down step undoes its own up step. The downs, run last first:
  ups in the order 1234, downs 4321: back exactly where it started.
  ups in the order 1243, downs 3421: not where it started: lost phone.
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.

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
choice> 5
an order of the four steps, e.g. 2413> 2413
In the order 2413: id, name, mobile, email, verified.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  verified  no, no, no, no
Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.
Rolled back, last first: not where it started: lost phone.

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
choice> 5
an order of the four steps, e.g. 2413> 4321
In the order 4321: id, name, mobile, email, verified.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  verified  no, no, no, no
Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.
Rolled back, last first: not where it started: lost phone.

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
choice> 5
an order of the four steps, e.g. 2413> 1234
In the order 1234: id, name, mobile, email, verified, phone.
  id        c1, c2, c3, c4
  name      Ana, Ben, Cleo, Dan
  mobile    555-0101, 555-0102, -, 555-0104
  email     ana@shop, ben@mail, cleo@shop, dan@mail
  verified  yes, no, yes, no
  phone     -, -, -, -
Run again in the same order, nothing changes.
Rolled back, last first: back exactly where it started.

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
choice> 6
Bye.
What was typed (11 lines)
1
2
3
4
5
2413
5
4321
5
1234
6

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

eml
# P174 db migrations: a customers table and four idempotent steps - the
# table and the steps, two orders, the release rolled back, the migrations
# rolled back, and an order of your own.
import schema

def plain(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def pad(s, n):
    s => out
    while len(out) < n:
        out + " " => out
    return out

def joined(xs):
    "" => out
    for x in xs:
        if out != "":
            out + ", " => out
        out + x => out
    return out

def names(t):
    [] => out
    for c in t:
        out + [c[0]] => out
    return out

def show_table(t):
    for c in t:
        [] => vs
        for v in c[1]:
            if v == "":
                vs + ["-"] => vs
            else:
                vs + [v] => vs
        ("  " + pad(c[0], 10) + joined(vs)) ^0

def label(order):
    "" => s
    for k in order:
        s + str(k) => s
    return s

def show_start():
    "The customers table (- is empty):" ^0
    show_table(schema.start())
    "The migration steps, each with a down step that undoes it:" ^0
    for s in schema.steps():
        ("  " + str(s[0]) + "  up: " + pad(s[1], 46) + "down: " + s[2]) ^0
    "Each step is idempotent: run twice in a row, the second run changes nothing." ^0

def show_orders():
    schema.start() => t0
    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:
        schema.run_up(t0, order) => t
        ("In the order " + label(order) + ": " + joined(names(t)) + ".") ^0
        show_table(t)
    "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
    "Idempotence says what running a step again does. It says nothing about the order." ^0

def show_release():
    schema.run_up(schema.start(), [1, 2, 3, 4]) => t
    "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
    schema.find(t, "phone") => p
    0 => had
    for v in schema.start()[schema.find(schema.start(), "phone")][1]:
        if v != "":
            had + 1 => had
    0 => shows
    for v in t[p][1]:
        if v != "":
            shows + 1 => shows
    ("  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
    "  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read." ^0
    "Rolling back the code is not rolling back the data." ^0

def compare(a, b):
    # what b lost and changed, against a
    [] => lost
    [] => changed
    for c in a:
        schema.find(b, c[0]) => i
        if i < 0:
            lost + [c[0]] => lost
        elif b[i][1] != c[1]:
            changed + [c[0]] => changed
    [] => extra
    for c in b:
        if schema.find(a, c[0]) < 0:
            extra + [c[0]] => extra
    return [lost, changed, extra]

def describe_down(order):
    schema.start() => t0
    schema.run_down(schema.run_up(t0, order), order) => back
    compare(t0, back) => d
    if len(d[0]) == 0 and len(d[1]) == 0 and len(d[2]) == 0:
        return "back exactly where it started."
    "" => s
    if len(d[0]) > 0:
        s + "lost " + joined(d[0]) => s
    if len(d[1]) > 0:
        if s != "":
            s + "; " => s
        s + "changed " + joined(d[1]) => s
    if len(d[2]) > 0:
        if s != "":
            s + "; " => s
        s + "left behind " + joined(d[2]) => s
    return "not where it started: " + s + "."

def show_down():
    "Each down step undoes its own up step. The downs, run last first:" ^0
    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:
        ("  ups in the order " + label(order) + ", downs " + label([order[3], order[2], order[1], order[0]]) + ": " + describe_down(order)) ^0
    "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

def own():
    plain(input("an order of the four steps, e.g. 2413> ")) => t
    if t == "":
        "Cancelled." ^0
        return
    [] => order
    True => ok
    if len(t) != 4:
        False => ok
    for c in t:
        if c < "1" or c > "4":
            False => ok
        elif ok:
            int(c) => k
            for x in order:
                if x == k:
                    False => ok
            order + [k] => order
    if not ok or len(order) != 4:
        "Type the digits 1 to 4, each once." ^0
        return
    schema.start() => t0
    schema.run_up(t0, order) => t1
    ("In the order " + t + ": " + joined(names(t1)) + ".") ^0
    show_table(t1)
    if schema.same(schema.run_up(t1, order), t1):
        "Run again in the same order, nothing changes." ^0
    else:
        "Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not." ^0
    ("Rolled back, last first: " + describe_down(order)) ^0

"== DB migrations ==" ^0
"Four idempotent migration steps on a customers table, and what their order and their rollbacks do." ^0
True => running
while running:
    "" ^0
    "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
    plain(input("choice> ")) => choice
    if choice == "1":
        show_start()
    elif choice == "2":
        show_orders()
    elif choice == "3":
        show_release()
    elif choice == "4":
        show_down()
    elif choice == "5":
        own()
    elif choice == "6":
        False => running
    else:
        "Pick a number from 1 to 6." ^0
"Bye." ^0
Python projection (main.py)
import schema

def plain(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def pad(s, n):
    out = s
    while len(out) < n:
        out = out + " "
    return out

def joined(xs):
    out = ""
    for x in xs:
        if out != "":
            out = out + ", "
        out = out + x
    return out

def names(t):
    out = []
    for c in t:
        out = out + [c[0]]
    return out

def show_table(t):
    for c in t:
        vs = []
        for v in c[1]:
            if v == "":
                vs = vs + ["-"]
            else:
                vs = vs + [v]
        print("  " + pad(c[0], 10) + joined(vs))

def label(order):
    s = ""
    for k in order:
        s = s + str(k)
    return s

def show_start():
    print("The customers table (- is empty):")
    show_table(schema.start())
    print("The migration steps, each with a down step that undoes it:")
    for s in schema.steps():
        print("  " + str(s[0]) + "  up: " + pad(s[1], 46) + "down: " + s[2])
    print("Each step is idempotent: run twice in a row, the second run changes nothing.")

def show_orders():
    t0 = schema.start()
    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:
        t = schema.run_up(t0, order)
        print("In the order " + label(order) + ": " + joined(names(t)) + ".")
        show_table(t)
    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.")
    print("Idempotence says what running a step again does. It says nothing about the order.")

def show_release():
    t = schema.run_up(schema.start(), [1, 2, 3, 4])
    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:")
    p = schema.find(t, "phone")
    had = 0
    for v in schema.start()[schema.find(schema.start(), "phone")][1]:
        if v != "":
            had = had + 1
    shows = 0
    for v in t[p][1]:
        if v != "":
            shows = shows + 1
    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.")
    print("  had the steps run in the order 1243, release 1 would find no phone column at all, and fail on its first read.")
    print("Rolling back the code is not rolling back the data.")

def compare(a, b):
    lost = []
    changed = []
    for c in a:
        i = schema.find(b, c[0])
        if i < 0:
            lost = lost + [c[0]]
        elif b[i][1] != c[1]:
            changed = changed + [c[0]]
    extra = []
    for c in b:
        if schema.find(a, c[0]) < 0:
            extra = extra + [c[0]]
    return [lost, changed, extra]

def describe_down(order):
    t0 = schema.start()
    back = schema.run_down(schema.run_up(t0, order), order)
    d = compare(t0, back)
    if len(d[0]) == 0 and len(d[1]) == 0 and len(d[2]) == 0:
        return "back exactly where it started."
    s = ""
    if len(d[0]) > 0:
        s = s + "lost " + joined(d[0])
    if len(d[1]) > 0:
        if s != "":
            s = s + "; "
        s = s + "changed " + joined(d[1])
    if len(d[2]) > 0:
        if s != "":
            s = s + "; "
        s = s + "left behind " + joined(d[2])
    return "not where it started: " + s + "."

def show_down():
    print("Each down step undoes its own up step. The downs, run last first:")
    for order in [[1, 2, 3, 4], [1, 2, 4, 3]]:
        print("  ups in the order " + label(order) + ", downs " + label([order[3], order[2], order[1], order[0]]) + ": " + describe_down(order))
    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.")

def own():
    t = plain(input("an order of the four steps, e.g. 2413> "))
    if t == "":
        print("Cancelled.")
        return
    order = []
    ok = True
    if len(t) != 4:
        ok = False
    for c in t:
        if c < "1" or c > "4":
            ok = False
        elif ok:
            k = int(c)
            for x in order:
                if x == k:
                    ok = False
            order = order + [k]
    if not ok or len(order) != 4:
        print("Type the digits 1 to 4, each once.")
        return
    t0 = schema.start()
    t1 = schema.run_up(t0, order)
    print("In the order " + t + ": " + joined(names(t1)) + ".")
    show_table(t1)
    if schema.same(schema.run_up(t1, order), t1):
        print("Run again in the same order, nothing changes.")
    else:
        print("Run again in the same order, it changes again: every step is idempotent, and the sequence run twice is not.")
    print("Rolled back, last first: " + describe_down(order))

print("== DB migrations ==")
print("Four idempotent migration steps on a customers table, and what their order and their rollbacks do.")
running = True
while running:
    print("")
    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")
    choice = plain(input("choice> "))
    if choice == "1":
        show_start()
    elif choice == "2":
        show_orders()
    elif choice == "3":
        show_release()
    elif choice == "4":
        show_down()
    elif choice == "5":
        own()
    elif choice == "6":
        running = False
    else:
        print("Pick a number from 1 to 6.")
print("Bye.")

schema.eml

eml
# P174 db migrations - a customers table and four migration steps, each one
# idempotent and each with a down step that undoes it. As in the corpus case
# the-migration-was-idempotent-and-the-order-was-not, running every step
# twice changes nothing, and running them in another order ends somewhere
# else. As in the-deploy-rolled-back-and-the-migration-did-not, rolling the
# code back leaves the schema where the migration put it. And as in
# the-migration-steps-were-each-reversible-and-the-sequence-was-not, each down
# step undoes its own up step, and the downs run in reverse do not undo the
# sequence. A table is a list of [column, values].

def start():
    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"]]]

def steps():
    # [number, what the up step does, what its down step does]
    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"]]

def copy(t):
    [] => out
    for c in t:
        [] => vs
        for v in c[1]:
            vs + [v] => vs
        out + [[c[0], vs]] => out
    return out

def find(t, name):
    0 => i
    while i < len(t):
        if t[i][0] == name:
            return i
        i + 1 => i
    return 0 - 1

def blank(n, v):
    [] => out
    while len(out) < n:
        out + [v] => out
    return out

def drop(t, name):
    [] => out
    for c in t:
        if c[0] != name:
            out + [c] => out
    return out

def ends_with(s, tail):
    if len(s) < len(tail):
        return False
    return s[len(s) - len(tail):] == tail

def up(t, k):
    # step k applied to table t: [new table, whether it changed anything]
    copy(t) => t
    len(t[0][1]) => n
    if k == 1:
        if find(t, "verified") >= 0:
            return [t, False]
        return [t + [["verified", blank(n, "no")]], True]
    if k == 2:
        find(t, "verified") => v
        find(t, "email") => e
        if v < 0:
            return [t, False]
        False => changed
        0 => i
        while i < n:
            if ends_with(t[e][1][i], "@shop") and t[v][1][i] != "yes":
                "yes" => t[v][1][i]
                True => changed
            i + 1 => i
        return [t, changed]
    if k == 3:
        find(t, "phone") => p
        if p < 0 or find(t, "mobile") >= 0:
            return [t, False]
        "mobile" => t[p][0]
        return [t, True]
    if find(t, "phone") >= 0:
        return [t, False]
    return [t + [["phone", blank(n, "")]], True]

def down(t, k):
    # step k's down step applied to table t
    copy(t) => t
    if k == 1:
        return drop(t, "verified")
    if k == 2:
        find(t, "verified") => v
        if v >= 0:
            blank(len(t[0][1]), "no") => t[v][1]
        return t
    if k == 3:
        find(t, "mobile") => m
        if m >= 0 and find(t, "phone") < 0:
            "phone" => t[m][0]
        return t
    return drop(t, "phone")

def run_up(t, order):
    for k in order:
        up(t, k)[0] => t
    return t

def run_down(t, order):
    # the down steps of order, last first
    len(order) - 1 => i
    while i >= 0:
        down(t, order[i]) => t
        i - 1 => i
    return t

def same(a, b):
    if len(a) != len(b):
        return False
    0 => i
    while i < len(a):
        if a[i][0] != b[i][0] or a[i][1] != b[i][1]:
            return False
        i + 1 => i
    return True
Python projection (schema.py)
def start():
    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"]]]

def steps():
    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"]]

def copy(t):
    out = []
    for c in t:
        vs = []
        for v in c[1]:
            vs = vs + [v]
        out = out + [[c[0], vs]]
    return out

def find(t, name):
    i = 0
    while i < len(t):
        if t[i][0] == name:
            return i
        i = i + 1
    return 0 - 1

def blank(n, v):
    out = []
    while len(out) < n:
        out = out + [v]
    return out

def drop(t, name):
    out = []
    for c in t:
        if c[0] != name:
            out = out + [c]
    return out

def ends_with(s, tail):
    if len(s) < len(tail):
        return False
    return s[len(s) - len(tail):] == tail

def up(t, k):
    t = copy(t)
    n = len(t[0][1])
    if k == 1:
        if find(t, "verified") >= 0:
            return [t, False]
        return [t + [["verified", blank(n, "no")]], True]
    if k == 2:
        v = find(t, "verified")
        e = find(t, "email")
        if v < 0:
            return [t, False]
        changed = False
        i = 0
        while i < n:
            if ends_with(t[e][1][i], "@shop") and t[v][1][i] != "yes":
                t[v][1][i] = "yes"
                changed = True
            i = i + 1
        return [t, changed]
    if k == 3:
        p = find(t, "phone")
        if p < 0 or find(t, "mobile") >= 0:
            return [t, False]
        t[p][0] = "mobile"
        return [t, True]
    if find(t, "phone") >= 0:
        return [t, False]
    return [t + [["phone", blank(n, "")]], True]

def down(t, k):
    t = copy(t)
    if k == 1:
        return drop(t, "verified")
    if k == 2:
        v = find(t, "verified")
        if v >= 0:
            t[v][1] = blank(len(t[0][1]), "no")
        return t
    if k == 3:
        m = find(t, "mobile")
        if m >= 0 and find(t, "phone") < 0:
            t[m][0] = "phone"
        return t
    return drop(t, "phone")

def run_up(t, order):
    for k in order:
        t = up(t, k)[0]
    return t

def run_down(t, order):
    i = len(order) - 1
    while i >= 0:
        t = down(t, order[i])
        i = i - 1
    return t

def same(a, b):
    if len(a) != len(b):
        return False
    i = 0
    while i < len(a):
        if a[i][0] != b[i][0] or a[i][1] != b[i][1]:
            return False
        i = i + 1
    return True

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.