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.
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 ownschema.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