{
  "id": "P143",
  "slug": "member-accounts",
  "key": "P143-member-accounts",
  "title": "Member accounts",
  "summary": "Sign-ups and soft deletes kept four ways - plain, partial, nulled and compound uniqueness - each decision set against the rule meant, one active account per address.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P143 - Member accounts\n\nSign-ups and soft deletes kept four ways, each decision set against the\nrule meant: at most one active account per address.\n\n- `main.eml` - the menu, the accounts in each scheme, signing up, deleting,\n  the clock, and the checks\n- `accounts.eml` - the four schemes, the key each unique index compares,\n  signing up, deleting, and counting the deleted rows and the addresses\n  they keep\n\nHow each part works:\n\n- A soft delete marks the row with the second it was deleted instead of\n  removing it. Ana, Bob and Cy have accounts; the clock is at second 2 and\n  moves on only when asked.\n- As in the corpus case `soft-delete-and-uniqueness`, the four schemes:\n  plain is unique on the address over every row; partial is unique among\n  active rows; nulled takes the address off a deleted row; compound is\n  unique on the address and the second it was deleted.\n- Plain refuses every sign-up of an address that only deleted accounts\n  have: its index still counts them. Partial makes every decision the rule\n  meant. Nulled decides the same but keeps no deleted address, so the\n  history no longer says whose the accounts were.\n- Compound decides right until one address is deleted twice in the same\n  second: the second deletion has the key of the first and fails, the\n  account stays active there, and the next sign-up of that address is\n  wrongly refused. A second later the deletion goes through.\n- No scheme ever lets two accounts with one address be active at once;\n  with no deletions all four decide the same, which is why a test that\n  never deletes and signs up again cannot tell them apart.\n\nWhat is checked: menu choices 1 to 6; an address of 3 to 12 characters,\nletters a to z with one @ not at either end; a deletion only of an address\nwith an active account. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the accounts; Bob deleted and signing up again; the checks;\n- in second 2: Ana's sign-up refused while active, then Ana deleted,\n  signing up, deleted again (compound fails) and signing up again;\n- in second 3: Ana deleted and signing up; the checks and the accounts.\n\n`sessions/bad-input.in` gives:\n- menu choices 0, x and 7;\n- addresses: a cancel, Ana@x, ab, a@b@c, @ab and one of 16 characters;\n- deleting: a cancel, zed@x (no active account) and x;\n- the next second, Dan signing up, and the checks with no deletions.\n\nBuilt on the verified corpus case `soft-delete-and-uniqueness` (four\nuniqueness schemes over sign-ups, deletions and re-sign-ups, wrong refusals\nand duplicates counted apart).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P143 member accounts: the same sign-ups and deletions kept four ways,\n# each decision set against the rule meant - at most one active account per\n# address - and the counts of what each way gets wrong.\nimport accounts\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 lpad(s, n):\n    s => out\n    while len(out) < n:\n        \" \" + out => out\n    return out\n\ndef address_ok(t):\n    # a to z with one @, not at either end, 3 to 12 characters\n    if len(t) < 3 or len(t) > 12:\n        return False\n    0 => ats\n    for c in t:\n        if c == \"@\":\n            ats + 1 => ats\n        elif c < \"a\" or c > \"z\":\n            return False\n    return ats == 1 and t[0] != \"@\" and t[len(t) - 1] != \"@\"\n\ndef times(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef show(rows, truth, t):\n    (\"It is second \" + str(t) + \".\") ^0\n    0 => i\n    for s in accounts.schemes():\n        (s + \" (\" + accounts.what()[i] + \"):\") ^0\n        for r in rows[s]:\n            if r[0] == \"\":\n                \"(no address)\" => a\n            else:\n                r[0] => a\n            if r[1] == 0:\n                (\"  \" + pad(a, 14) + \"active\") ^0\n            else:\n                (\"  \" + pad(a, 14) + \"deleted in second \" + str(r[1])) ^0\n        i + 1 => i\n\ndef signup(rows, truth, counts, t):\n    plain(input(\"address> \")) => a\n    if a == \"\":\n        \"Cancelled.\" ^0\n        return [rows, truth, counts]\n    if not address_ok(a):\n        \"Type an address like ana@x: letters a to z with one @, 3 to 12 characters.\" ^0\n        return [rows, truth, counts]\n    accounts.active(truth, a) < 0 => meant\n    if meant:\n        (\"Sign-up for \" + a + \" in second \" + str(t) + \" - the rule meant: accept, no active account has it.\") ^0\n    else:\n        (\"Sign-up for \" + a + \" in second \" + str(t) + \" - the rule meant: refuse, an active account has it.\") ^0\n    {} => out\n    for s in accounts.schemes():\n        accounts.signup(s, rows[s], a) => r\n        r[0] => out[s]\n        if r[1] == \"ok\":\n            \"accepted\" => said\n        elif r[1] == \"active\":\n            \"refused: an active account has the address\" => said\n        else:\n            \"refused: a deleted account still has the address\" => said\n        if meant and r[1] != \"ok\":\n            said + \"   <- wrong\" => said\n            counts[s + \" wrong\"] + 1 => counts[s + \" wrong\"]\n        if not meant and r[1] == \"ok\":\n            said + \"   <- wrong: two active accounts\" => said\n            counts[s + \" dup\"] + 1 => counts[s + \" dup\"]\n        (\"  \" + pad(s, 10) + said) ^0\n    if meant:\n        truth + [[a, 0]] => truth\n    return [out, truth, counts]\n\ndef delete(rows, truth, counts, t):\n    plain(input(\"address> \")) => a\n    if a == \"\":\n        \"Cancelled.\" ^0\n        return [rows, truth, counts]\n    if not address_ok(a):\n        \"Type an address like ana@x: letters a to z with one @, 3 to 12 characters.\" ^0\n        return [rows, truth, counts]\n    if accounts.active(truth, a) < 0:\n        (\"No account with the address \" + a + \" is active.\") ^0\n        return [rows, truth, counts]\n    (\"Deleting \" + a + \" in second \" + str(t) + \":\") ^0\n    {} => out\n    for s in accounts.schemes():\n        accounts.delete(s, rows[s], a, t) => r\n        r[0] => out[s]\n        if r[1] == \"ok\":\n            \"deleted\" => said\n        elif r[1] == \"none\":\n            \"nothing to delete: no active account has the address here\" => said\n        else:\n            \"the deletion fails: a row deleted in second \" + str(t) + \" already has the key \" + accounts.key(s, [a, t]) => said\n            counts[s + \" failed\"] + 1 => counts[s + \" failed\"]\n        (\"  \" + pad(s, 10) + said) ^0\n    accounts.delete(\"partial\", truth, a, t)[0] => truth\n    return [out, truth, counts]\n\ndef checks(rows, counts):\n    \"                wrongly   two active   deletions   deleted rows\" ^0\n    \"                refused   allowed      failed      keeping the address\" ^0\n    [] => right\n    for s in accounts.schemes():\n        [counts[s + \" wrong\"], counts[s + \" dup\"], counts[s + \" failed\"]] => c\n        accounts.kept(rows[s]) => k\n        accounts.deleted(rows[s]) => d\n        (\"  \" + pad(s, 12) + lpad(str(c[0]), 6) + lpad(str(c[1]), 11) + lpad(str(c[2]), 13) + \"      \" + str(k) + \" of \" + str(d)) ^0\n        if c[0] == 0 and c[1] == 0 and c[2] == 0 and k == d:\n            right + [s] => right\n    \"The rule meant: at most one active account per address.\" ^0\n    if len(right) > 0:\n        \"\" => names\n        for s in right:\n            if names == \"\":\n                s => names\n            else:\n                names + \", \" + s => names\n        (\"Right on every count so far: \" + names + \".\") ^0\n    counts[\"plain wrong\"] => c\n    if c > 0:\n        (\"plain refused \" + times(c, \"sign-up\", \"sign-ups\") + \" the rule allows: its index still counts the deleted rows.\") ^0\n    accounts.kept(rows[\"nulled\"]) => k\n    accounts.deleted(rows[\"nulled\"]) => d\n    if k < d:\n        (\"nulled decides right, and kept the address of \" + str(k) + \" of \" + times(d, \"deleted account\", \"deleted accounts\") + \": the history no longer says whose they were.\") ^0\n    if counts[\"compound failed\"] > 0:\n        (\"compound failed \" + times(counts[\"compound failed\"], \"deletion\", \"deletions\") + \": two deletions of one address in the same second have the same key.\") ^0\n    if counts[\"compound wrong\"] > 0:\n        (\"compound refused \" + times(counts[\"compound wrong\"], \"sign-up\", \"sign-ups\") + \" the rule allows: an account whose deletion failed is still active there.\") ^0\n\n{} => rows\n{} => counts\n[] => truth\nfor s in accounts.schemes():\n    [] => rows[s]\n    0 => counts[s + \" wrong\"]\n    0 => counts[s + \" dup\"]\n    0 => counts[s + \" failed\"]\nfor a in accounts.opening():\n    for s in accounts.schemes():\n        accounts.signup(s, rows[s], a)[0] => rows[s]\n    truth + [[a, 0]] => truth\n2 => t\n\n\"== Member accounts ==\" ^0\n\"Sign-ups and soft deletes kept four ways, against the rule meant: one active account per address.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show(rows, truth, t)\n    elif choice == \"2\":\n        signup(rows, truth, counts, t) => r\n        r[0] => rows\n        r[1] => truth\n        r[2] => counts\n    elif choice == \"3\":\n        delete(rows, truth, counts, t) => r\n        r[0] => rows\n        r[1] => truth\n        r[2] => counts\n    elif choice == \"4\":\n        t + 1 => t\n        (\"It is second \" + str(t) + \".\") ^0\n    elif choice == \"5\":\n        checks(rows, counts)\n    elif choice == \"6\":\n        False => running\n    else:\n        \"Pick a number from 1 to 6.\" ^0\n\"Bye.\" ^0\n",
      "python": "import accounts\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 lpad(s, n):\n    out = s\n    while len(out) < n:\n        out = \" \" + out\n    return out\n\ndef address_ok(t):\n    if len(t) < 3 or len(t) > 12:\n        return False\n    ats = 0\n    for c in t:\n        if c == \"@\":\n            ats = ats + 1\n        elif c < \"a\" or c > \"z\":\n            return False\n    return ats == 1 and t[0] != \"@\" and t[len(t) - 1] != \"@\"\n\ndef times(n, one, many):\n    if n == 1:\n        return \"1 \" + one\n    return str(n) + \" \" + many\n\ndef show(rows, truth, t):\n    print(\"It is second \" + str(t) + \".\")\n    i = 0\n    for s in accounts.schemes():\n        print(s + \" (\" + accounts.what()[i] + \"):\")\n        for r in rows[s]:\n            if r[0] == \"\":\n                a = \"(no address)\"\n            else:\n                a = r[0]\n            if r[1] == 0:\n                print(\"  \" + pad(a, 14) + \"active\")\n            else:\n                print(\"  \" + pad(a, 14) + \"deleted in second \" + str(r[1]))\n        i = i + 1\n\ndef signup(rows, truth, counts, t):\n    a = plain(input(\"address> \"))\n    if a == \"\":\n        print(\"Cancelled.\")\n        return [rows, truth, counts]\n    if not address_ok(a):\n        print(\"Type an address like ana@x: letters a to z with one @, 3 to 12 characters.\")\n        return [rows, truth, counts]\n    meant = accounts.active(truth, a) < 0\n    if meant:\n        print(\"Sign-up for \" + a + \" in second \" + str(t) + \" - the rule meant: accept, no active account has it.\")\n    else:\n        print(\"Sign-up for \" + a + \" in second \" + str(t) + \" - the rule meant: refuse, an active account has it.\")\n    out = {}\n    for s in accounts.schemes():\n        r = accounts.signup(s, rows[s], a)\n        out[s] = r[0]\n        if r[1] == \"ok\":\n            said = \"accepted\"\n        elif r[1] == \"active\":\n            said = \"refused: an active account has the address\"\n        else:\n            said = \"refused: a deleted account still has the address\"\n        if meant and r[1] != \"ok\":\n            said = said + \"   <- wrong\"\n            counts[s + \" wrong\"] = counts[s + \" wrong\"] + 1\n        if not meant and r[1] == \"ok\":\n            said = said + \"   <- wrong: two active accounts\"\n            counts[s + \" dup\"] = counts[s + \" dup\"] + 1\n        print(\"  \" + pad(s, 10) + said)\n    if meant:\n        truth = truth + [[a, 0]]\n    return [out, truth, counts]\n\ndef delete(rows, truth, counts, t):\n    a = plain(input(\"address> \"))\n    if a == \"\":\n        print(\"Cancelled.\")\n        return [rows, truth, counts]\n    if not address_ok(a):\n        print(\"Type an address like ana@x: letters a to z with one @, 3 to 12 characters.\")\n        return [rows, truth, counts]\n    if accounts.active(truth, a) < 0:\n        print(\"No account with the address \" + a + \" is active.\")\n        return [rows, truth, counts]\n    print(\"Deleting \" + a + \" in second \" + str(t) + \":\")\n    out = {}\n    for s in accounts.schemes():\n        r = accounts.delete(s, rows[s], a, t)\n        out[s] = r[0]\n        if r[1] == \"ok\":\n            said = \"deleted\"\n        elif r[1] == \"none\":\n            said = \"nothing to delete: no active account has the address here\"\n        else:\n            said = \"the deletion fails: a row deleted in second \" + str(t) + \" already has the key \" + accounts.key(s, [a, t])\n            counts[s + \" failed\"] = counts[s + \" failed\"] + 1\n        print(\"  \" + pad(s, 10) + said)\n    truth = accounts.delete(\"partial\", truth, a, t)[0]\n    return [out, truth, counts]\n\ndef checks(rows, counts):\n    print(\"                wrongly   two active   deletions   deleted rows\")\n    print(\"                refused   allowed      failed      keeping the address\")\n    right = []\n    for s in accounts.schemes():\n        c = [counts[s + \" wrong\"], counts[s + \" dup\"], counts[s + \" failed\"]]\n        k = accounts.kept(rows[s])\n        d = accounts.deleted(rows[s])\n        print(\"  \" + pad(s, 12) + lpad(str(c[0]), 6) + lpad(str(c[1]), 11) + lpad(str(c[2]), 13) + \"      \" + str(k) + \" of \" + str(d))\n        if c[0] == 0 and c[1] == 0 and c[2] == 0 and k == d:\n            right = right + [s]\n    print(\"The rule meant: at most one active account per address.\")\n    if len(right) > 0:\n        names = \"\"\n        for s in right:\n            if names == \"\":\n                names = s\n            else:\n                names = names + \", \" + s\n        print(\"Right on every count so far: \" + names + \".\")\n    c = counts[\"plain wrong\"]\n    if c > 0:\n        print(\"plain refused \" + times(c, \"sign-up\", \"sign-ups\") + \" the rule allows: its index still counts the deleted rows.\")\n    k = accounts.kept(rows[\"nulled\"])\n    d = accounts.deleted(rows[\"nulled\"])\n    if k < d:\n        print(\"nulled decides right, and kept the address of \" + str(k) + \" of \" + times(d, \"deleted account\", \"deleted accounts\") + \": the history no longer says whose they were.\")\n    if counts[\"compound failed\"] > 0:\n        print(\"compound failed \" + times(counts[\"compound failed\"], \"deletion\", \"deletions\") + \": two deletions of one address in the same second have the same key.\")\n    if counts[\"compound wrong\"] > 0:\n        print(\"compound refused \" + times(counts[\"compound wrong\"], \"sign-up\", \"sign-ups\") + \" the rule allows: an account whose deletion failed is still active there.\")\n\nrows = {}\ncounts = {}\ntruth = []\nfor s in accounts.schemes():\n    rows[s] = []\n    counts[s + \" wrong\"] = 0\n    counts[s + \" dup\"] = 0\n    counts[s + \" failed\"] = 0\nfor a in accounts.opening():\n    for s in accounts.schemes():\n        rows[s] = accounts.signup(s, rows[s], a)[0]\n    truth = truth + [[a, 0]]\nt = 2\nprint(\"== Member accounts ==\")\nprint(\"Sign-ups and soft deletes kept four ways, against the rule meant: one active account per address.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show(rows, truth, t)\n    elif choice == \"2\":\n        r = signup(rows, truth, counts, t)\n        rows = r[0]\n        truth = r[1]\n        counts = r[2]\n    elif choice == \"3\":\n        r = delete(rows, truth, counts, t)\n        rows = r[0]\n        truth = r[1]\n        counts = r[2]\n    elif choice == \"4\":\n        t = t + 1\n        print(\"It is second \" + str(t) + \".\")\n    elif choice == \"5\":\n        checks(rows, counts)\n    elif choice == \"6\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 6.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "accounts.eml",
      "eml": "# P143 member accounts - one table of accounts kept four ways. As in the\n# corpus case soft-delete-and-uniqueness, a soft delete marks a row instead\n# of removing it, and a unique index on the address still counts the\n# deleted rows: the rule meant is \"at most one ACTIVE account per address\".\n# A row is [address, second it was deleted]; 0 means active.\n#   plain     unique on the address over every row\n#   partial   unique on the address among active rows\n#   nulled    a deleted row loses its address; unique over the rest\n#   compound  unique on the address and the second it was deleted\n\ndef schemes():\n    return [\"plain\", \"partial\", \"nulled\", \"compound\"]\n\ndef what():\n    return [\"unique on the address over every row\", \"unique on the address among active rows\", \"a deleted row loses its address\", \"unique on the address and the second it was deleted\"]\n\ndef key(scheme, row):\n    # what the unique index compares; \"\" is not compared at all\n    if scheme == \"partial\":\n        if row[1] > 0:\n            return \"\"\n        return row[0]\n    if scheme == \"compound\":\n        return row[0] + \"@\" + str(row[1])\n    return row[0]\n\ndef clash(scheme, rows, k, skip):\n    # another row, not the one at position skip, with the same key\n    if k == \"\":\n        return False\n    0 => i\n    for r in rows:\n        if i != skip and key(scheme, r) == k:\n            return True\n        i + 1 => i\n    return False\n\ndef active(rows, address):\n    0 => i\n    for r in rows:\n        if r[0] == address and r[1] == 0:\n            return i\n        i + 1 => i\n    return 0 - 1\n\ndef signup(scheme, rows, address):\n    # [rows, \"ok\"], or the rows unchanged with \"active\" or \"taken\"\n    [address, 0] => row\n    if clash(scheme, rows, key(scheme, row), 0 - 1):\n        if active(rows, address) >= 0:\n            return [rows, \"active\"]\n        return [rows, \"taken\"]\n    return [rows + [row], \"ok\"]\n\ndef delete(scheme, rows, address, t):\n    # [rows, \"ok\"], or the rows unchanged with \"none\" or \"clash\"\n    active(rows, address) => i\n    if i < 0:\n        return [rows, \"none\"]\n    if scheme == \"nulled\":\n        [\"\", t] => row\n    else:\n        [address, t] => row\n    if clash(scheme, rows, key(scheme, row), i):\n        return [rows, \"clash\"]\n    [] => out\n    0 => j\n    for r in rows:\n        if j == i:\n            out + [row] => out\n        else:\n            out + [r] => out\n        j + 1 => j\n    return [out, \"ok\"]\n\ndef kept(rows):\n    # deleted rows that still record their address\n    0 => n\n    for r in rows:\n        if r[1] > 0 and r[0] != \"\":\n            n + 1 => n\n    return n\n\ndef deleted(rows):\n    0 => n\n    for r in rows:\n        if r[1] > 0:\n            n + 1 => n\n    return n\n\ndef opening():\n    return [\"ana@x\", \"bob@x\", \"cy@x\"]\n",
      "python": "def schemes():\n    return [\"plain\", \"partial\", \"nulled\", \"compound\"]\n\ndef what():\n    return [\"unique on the address over every row\", \"unique on the address among active rows\", \"a deleted row loses its address\", \"unique on the address and the second it was deleted\"]\n\ndef key(scheme, row):\n    if scheme == \"partial\":\n        if row[1] > 0:\n            return \"\"\n        return row[0]\n    if scheme == \"compound\":\n        return row[0] + \"@\" + str(row[1])\n    return row[0]\n\ndef clash(scheme, rows, k, skip):\n    if k == \"\":\n        return False\n    i = 0\n    for r in rows:\n        if i != skip and key(scheme, r) == k:\n            return True\n        i = i + 1\n    return False\n\ndef active(rows, address):\n    i = 0\n    for r in rows:\n        if r[0] == address and r[1] == 0:\n            return i\n        i = i + 1\n    return 0 - 1\n\ndef signup(scheme, rows, address):\n    row = [address, 0]\n    if clash(scheme, rows, key(scheme, row), 0 - 1):\n        if active(rows, address) >= 0:\n            return [rows, \"active\"]\n        return [rows, \"taken\"]\n    return [rows + [row], \"ok\"]\n\ndef delete(scheme, rows, address, t):\n    i = active(rows, address)\n    if i < 0:\n        return [rows, \"none\"]\n    if scheme == \"nulled\":\n        row = [\"\", t]\n    else:\n        row = [address, t]\n    if clash(scheme, rows, key(scheme, row), i):\n        return [rows, \"clash\"]\n    out = []\n    j = 0\n    for r in rows:\n        if j == i:\n            out = out + [row]\n        else:\n            out = out + [r]\n        j = j + 1\n    return [out, \"ok\"]\n\ndef kept(rows):\n    n = 0\n    for r in rows:\n        if r[1] > 0 and r[0] != \"\":\n            n = n + 1\n    return n\n\ndef deleted(rows):\n    n = 0\n    for r in rows:\n        if r[1] > 0:\n            n = n + 1\n    return n\n\ndef opening():\n    return [\"ana@x\", \"bob@x\", \"cy@x\"]\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n7\n2\n\n2\nAna@x\n2\nab\n2\na@b@c\n2\n@ab\n2\ntoolongaddress@x\n3\n\n3\nzed@x\n3\nx\n4\n2\ndan@x\n5\n6\n",
      "screen": "== Member accounts ==\nSign-ups and soft deletes kept four ways, against the rule meant: one active account per address.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 0\nPick a number from 1 to 6.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> x\nPick a number from 1 to 6.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 7\nPick a number from 1 to 6.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> \nCancelled.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> Ana@x\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> ab\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> a@b@c\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> @ab\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> toolongaddress@x\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> \nCancelled.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> zed@x\nNo account with the address zed@x is active.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> x\nType an address like ana@x: letters a to z with one @, 3 to 12 characters.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 4\nIt is second 3.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> dan@x\nSign-up for dan@x in second 3 - the rule meant: accept, no active account has it.\n  plain     accepted\n  partial   accepted\n  nulled    accepted\n  compound  accepted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 5\n                wrongly   two active   deletions   deleted rows\n                refused   allowed      failed      keeping the address\n  plain            0          0            0      0 of 0\n  partial          0          0            0      0 of 0\n  nulled           0          0            0      0 of 0\n  compound         0          0            0      0 of 0\nThe rule meant: at most one active account per address.\nRight on every count so far: plain, partial, nulled, compound.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n3\nbob@x\n2\nbob@x\n5\n2\nana@x\n3\nana@x\n2\nana@x\n3\nana@x\n2\nana@x\n4\n3\nana@x\n2\nana@x\n5\n1\n6\n",
      "screen": "== Member accounts ==\nSign-ups and soft deletes kept four ways, against the rule meant: one active account per address.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 1\nIt is second 2.\nplain (unique on the address over every row):\n  ana@x         active\n  bob@x         active\n  cy@x          active\npartial (unique on the address among active rows):\n  ana@x         active\n  bob@x         active\n  cy@x          active\nnulled (a deleted row loses its address):\n  ana@x         active\n  bob@x         active\n  cy@x          active\ncompound (unique on the address and the second it was deleted):\n  ana@x         active\n  bob@x         active\n  cy@x          active\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> bob@x\nDeleting bob@x in second 2:\n  plain     deleted\n  partial   deleted\n  nulled    deleted\n  compound  deleted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> bob@x\nSign-up for bob@x in second 2 - the rule meant: accept, no active account has it.\n  plain     refused: a deleted account still has the address   <- wrong\n  partial   accepted\n  nulled    accepted\n  compound  accepted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 5\n                wrongly   two active   deletions   deleted rows\n                refused   allowed      failed      keeping the address\n  plain            1          0            0      1 of 1\n  partial          0          0            0      1 of 1\n  nulled           0          0            0      0 of 1\n  compound         0          0            0      1 of 1\nThe rule meant: at most one active account per address.\nRight on every count so far: partial, compound.\nplain refused 1 sign-up the rule allows: its index still counts the deleted rows.\nnulled decides right, and kept the address of 0 of 1 deleted account: the history no longer says whose they were.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> ana@x\nSign-up for ana@x in second 2 - the rule meant: refuse, an active account has it.\n  plain     refused: an active account has the address\n  partial   refused: an active account has the address\n  nulled    refused: an active account has the address\n  compound  refused: an active account has the address\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> ana@x\nDeleting ana@x in second 2:\n  plain     deleted\n  partial   deleted\n  nulled    deleted\n  compound  deleted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> ana@x\nSign-up for ana@x in second 2 - the rule meant: accept, no active account has it.\n  plain     refused: a deleted account still has the address   <- wrong\n  partial   accepted\n  nulled    accepted\n  compound  accepted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> ana@x\nDeleting ana@x in second 2:\n  plain     nothing to delete: no active account has the address here\n  partial   deleted\n  nulled    deleted\n  compound  the deletion fails: a row deleted in second 2 already has the key ana@x@2\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> ana@x\nSign-up for ana@x in second 2 - the rule meant: accept, no active account has it.\n  plain     refused: a deleted account still has the address   <- wrong\n  partial   accepted\n  nulled    accepted\n  compound  refused: an active account has the address   <- wrong\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 4\nIt is second 3.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 3\naddress> ana@x\nDeleting ana@x in second 3:\n  plain     nothing to delete: no active account has the address here\n  partial   deleted\n  nulled    deleted\n  compound  deleted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 2\naddress> ana@x\nSign-up for ana@x in second 3 - the rule meant: accept, no active account has it.\n  plain     refused: a deleted account still has the address   <- wrong\n  partial   accepted\n  nulled    accepted\n  compound  accepted\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 5\n                wrongly   two active   deletions   deleted rows\n                refused   allowed      failed      keeping the address\n  plain            4          0            0      2 of 2\n  partial          0          0            0      4 of 4\n  nulled           0          0            0      0 of 4\n  compound         1          0            1      3 of 3\nThe rule meant: at most one active account per address.\nRight on every count so far: partial.\nplain refused 4 sign-ups the rule allows: its index still counts the deleted rows.\nnulled decides right, and kept the address of 0 of 4 deleted accounts: the history no longer says whose they were.\ncompound failed 1 deletion: two deletions of one address in the same second have the same key.\ncompound refused 1 sign-up the rule allows: an account whose deletion failed is still active there.\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 1\nIt is second 3.\nplain (unique on the address over every row):\n  ana@x         deleted in second 2\n  bob@x         deleted in second 2\n  cy@x          active\npartial (unique on the address among active rows):\n  ana@x         deleted in second 2\n  bob@x         deleted in second 2\n  cy@x          active\n  bob@x         active\n  ana@x         deleted in second 2\n  ana@x         deleted in second 3\n  ana@x         active\nnulled (a deleted row loses its address):\n  (no address)  deleted in second 2\n  (no address)  deleted in second 2\n  cy@x          active\n  bob@x         active\n  (no address)  deleted in second 2\n  (no address)  deleted in second 3\n  ana@x         active\ncompound (unique on the address and the second it was deleted):\n  ana@x         deleted in second 2\n  bob@x         deleted in second 2\n  cy@x          active\n  bob@x         active\n  ana@x         deleted in second 3\n  ana@x         active\n\n1) accounts  2) sign up  3) delete  4) next second  5) checks  6) quit\nchoice> 6\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "soft-delete-and-uniqueness",
      "caseId": "288-soft-delete-and-uniqueness",
      "title": "Soft delete and uniqueness — the row is gone and the address is taken"
    }
  ],
  "updated": "2026-10-18"
}
