mattrguerra/meridian-ops-boardmainPrivate on GitHub, published here in full

0002_own_list.sql

Language SQL8.6 KB158 lines

apps/worker/migrations/0002_own_list.sql

  1. -- One person's own private list: the lines they log for themselves so they do
  2. -- not lose them. The only table on this board with exactly one reader (rule 15).
  3. --
  4. -- It sits in the My Day card beside the work the five sources already assign
  5. -- them, and nobody else can see it. There is no reader list and no leadership
  6. -- view.
  7. --
  8. -- D1 and not KV: 0001's argument, and a sharper one.
  9. --
  10. -- 0001 chose a table because the end-of-day note is a series: a row a day, read
  11. -- back as a range, where read-modify-write over a whole KV blob can lose a
  12. -- submission to eventual consistency (up to about a minute). All of that is
  13. -- true here and worse: everyone on the roster writes to this one, not one
  14. -- person, so two of them adding a line in the same minute would race each
  15. -- other through a shared blob, and the thing lost is the only copy of something
  16. -- somebody typed once.
  17. --
  18. -- The same database as the end-of-day note: `meridian-checkins`, binding
  19. -- CHECKINS, migrations_dir `migrations`. One database means one
  20. -- `wrangler d1 migrations apply`, one optional-binding story, and one "not set
  21. -- up" sentence to get right rather than two. The binding name is wider than
  22. -- its first table, and that is a recorded trade rather than an oversight: it
  23. -- is deployed config, and renaming it would touch every failure sentence in
  24. -- checkin.ts. A misnomer is findable in one grep; a half-applied rename is
  25. -- not. If a third D1 feature lands, rename it then, on its own.
  26. --
  27. -- No revision table, unlike 0001, deliberately. `checkin_revision` exists
  28. -- because a second person reads the note, and an edit
  29. -- would silently replace text they had already read. Nobody but the author ever
  30. -- reads this table. Keeping superseded versions here would mean storing private
  31. -- text its owner believed they had changed, forever, on a surface whose entire
  32. -- premise is that it is safe to write in. `edits` records that a line changed,
  33. -- which is all anybody here needs. Do not add one for consistency.
  34. --
  35. -- Apply it (the database already exists; this is a second migration on it):
  36. -- cd apps/worker
  37. -- npx wrangler d1 migrations apply meridian-checkins --local # a dry run
  38. -- npx wrangler d1 migrations apply meridian-checkins --remote # production
  39. --
  40. -- Apply it before deploying the Worker. The other order gives every person a
  41. -- card reading "your list could not be read: no such table: own_item", which
  42. -- is honest, alarming and entirely avoidable. Until the migration lands the
  43. -- board says the store could not be read, in SQLite's own words and with no
  44. -- diagnosis (rule 1); with no binding at all it says not set up. Those are
  45. -- different facts and they have different sentences.
  46. CREATE TABLE IF NOT EXISTS own_item (
  47. -- Autoincrement: ids are never reused. The complete, reopen and edit forms
  48. -- each carry an id, and a tab left open while a line is completed would
  49. -- otherwise be able to act on whatever row inherited the number.
  50. id INTEGER PRIMARY KEY AUTOINCREMENT,
  51. -- The roster display name, and 0001's argument at its strongest.
  52. --
  53. -- Most addresses here are role mailboxes. When accounts@ changes hands, the
  54. -- next holder signs in as the same string. Keyed by address, that person
  55. -- would open the board and read the previous holder's private list: not a mis-attributed count, an actual disclosure of
  56. -- somebody's own words to somebody who was never meant to have them. Keyed by
  57. -- roster name, a seat change starts an empty list and leaves the old one
  58. -- intact, correctly attributed, and unreachable.
  59. --
  60. -- Consequence, written down rather than discovered later: renaming a person
  61. -- in config/meridian-people.ts orphans their rows. The rows stay and stay
  62. -- correct; they stop being reachable.
  63. person TEXT NOT NULL,
  64. -- What actually authenticated, recorded beside the name rather than instead
  65. -- of it. If these two ever disagree with the roster, this is the evidence of
  66. -- which mailbox was signed in as.
  67. author_email TEXT NOT NULL,
  68. -- The line itself. Capped below; the route caps it too (OWN_TEXT_CAP).
  69. text TEXT NOT NULL,
  70. -- YYYY-MM-DD in `tz`, or NULL for "no date". NULL and a past date are
  71. -- different facts and the card renders them differently: a line with no date
  72. -- is not overdue and never becomes overdue. The GLOB is here because a
  73. -- malformed date would sort into the wrong place in every comparison and
  74. -- never throw.
  75. --
  76. -- A date, not a timestamp. A time implies something happens at that time, and
  77. -- nothing here notifies anybody; the card says so in as many words.
  78. due TEXT CHECK (due IS NULL OR due GLOB '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'),
  79. -- Three values, fixed, matching OWN_PRIORITIES in config/own-list.ts. TEXT
  80. -- plus a CHECK under STRICT, so a bad value is a refused write rather than a
  81. -- row that sorts oddly and renders blank. Not an integer: a number invites an
  82. -- average priority or a priority total, and there is no such quantity.
  83. --
  84. -- The route refuses an absent or unknown value rather than defaulting to one
  85. -- (rule 2). The form always sends one, so a missing one is a malformed post.
  86. priority TEXT NOT NULL CHECK (priority IN ('high', 'normal', 'low')),
  87. -- The optional reference to real work, as the same key the rest of the board
  88. -- already uses: 'order:MD2639582' or 'task:41837'. board:notes is keyed on
  89. -- this exact string and work rows already carry it in data-note-key, so a
  90. -- line can be shown against that order later with no translation.
  91. --
  92. -- One column, not two. A kind and an id in separate columns can be half
  93. -- filled; this cannot. NULL means no link. '' is refused, because an empty
  94. -- link and no link must not become two spellings of the same thing.
  95. --
  96. -- The CHECK is coarser than the route's regex, on purpose. GLOB cannot
  97. -- express the character class; WORK_KEY_RE in apps/dashboard/src/work-key.ts
  98. -- does, and it is the same regex /note validates with. This CHECK exists so
  99. -- that a shape nothing can render cannot be written at all.
  100. link_key TEXT CHECK (
  101. link_key IS NULL OR (
  102. length(link_key) BETWEEN 6 AND 46 AND
  103. (link_key GLOB 'order:[A-Za-z0-9_-]*' OR link_key GLOB 'task:[A-Za-z0-9_-]*')
  104. )
  105. ),
  106. created_at TEXT NOT NULL, -- ISO 8601 UTC, when it was logged
  107. -- ISO 8601 UTC, the version stored here. Also the concurrency token: every
  108. -- mutation carries the value the form was rendered with and is refused if it
  109. -- no longer matches. Same idea as the check-in's claimedDay, moved from a day
  110. -- to a row: refusing costs a reload, guessing costs the record.
  111. updated_at TEXT NOT NULL,
  112. edits INTEGER NOT NULL DEFAULT 0 CHECK (edits >= 0),
  113. -- NULL means open. A timestamp means done, and the line stays: completing
  114. -- something must never look like losing it, and reopening is one click. This
  115. -- is also what makes the whole feature reversible from the page that wrote it
  116. -- (rule 12) without a confirmation step anywhere.
  117. done_at TEXT,
  118. -- The zone `due` is judged against, so a row stays legible if the board's
  119. -- clock ever moves. Everything is America/Chicago (MERIDIAN_TZ).
  120. tz TEXT NOT NULL,
  121. -- A line must say something. A blank row renders as an empty bullet its owner
  122. -- cannot identify and cannot act on, and it would be counted in the "open"
  123. -- number above the list, rule 9 broken by a row that is technically present.
  124. -- The route refuses one too; this is the same rule at the storage layer,
  125. -- where nothing can route around it.
  126. --
  127. -- Both checks sit down here because they are table constraints. SQLite
  128. -- rejects the whole CREATE if one appears between two columns, which only
  129. -- executing the file reveals; that is why a test executes it.
  130. CHECK (length(trim(text)) > 0),
  131. CHECK (length(text) <= 500)
  132. ) STRICT;
  133. -- No unique constraint, and that is a decision rather than an omission. 0001
  134. -- has one because its upsert depends on it; there is no upsert here. A line is
  135. -- not identified by its text: "call the client back" logged twice in a week is
  136. -- two reminders, and collapsing them would silently delete the second.
  137. -- Identity is the id and nothing else.
  138. -- The open list, in the order the card reads it: this person, dated first.
  139. CREATE INDEX IF NOT EXISTS own_item_open
  140. ON own_item (person, due, id) WHERE done_at IS NULL;
  141. -- The done list, newest completion first. A separate partial index because
  142. -- these are two different questions asked by two different statements.
  143. CREATE INDEX IF NOT EXISTS own_item_done
  144. ON own_item (person, done_at DESC) WHERE done_at IS NOT NULL;