From ba96974bdebd6d4ec345907d49944b5ee644ed47 Mon Sep 17 00:00:00 2001
From: Owen Jacobson <owen@grimoire.ca>
Date: Wed, 9 Oct 2024 00:57:31 -0400
Subject: Provide a view of logins to clients.

---
 migrations/20241009031441_login_created_at.sql | 217 +++++++++++++++++++++++++
 1 file changed, 217 insertions(+)
 create mode 100644 migrations/20241009031441_login_created_at.sql

(limited to 'migrations/20241009031441_login_created_at.sql')

diff --git a/migrations/20241009031441_login_created_at.sql b/migrations/20241009031441_login_created_at.sql
new file mode 100644
index 0000000..001c48e
--- /dev/null
+++ b/migrations/20241009031441_login_created_at.sql
@@ -0,0 +1,217 @@
+-- figure out new event seqeuence.
+--
+-- The main problem here is ensuring that the resulting stream is _consistent_.
+-- For any message, the login it refers to should be created in the event stream
+-- before the message does, and the channel it refers to should likewise be
+-- created first.
+--
+-- Messages come after channels by time (clock slew allowing), so we can reuse
+-- those timestamps to recover a global ordering between channels and messages.
+--
+-- We synthesize a login's created_at from the earliest of:
+--
+-- * the earliest message they sent that we know of, or
+-- * right now (no messages in recorded history).
+--
+-- This produces a consistent sequence, up to clock slew, at the expense of
+-- renumbering every event.
+
+create table unsequenced (
+	at text
+		not null,
+	login text
+		references login (id),
+	channel text
+		references channel (id),
+	message text
+		references message (id),
+	check (
+		(login is not null and channel is null and message is null)
+		or (login is null and channel is not null and message is null)
+		or (login is null and channel is null and message is not null)
+	)
+);
+
+insert into unsequenced (at, login)
+select
+	coalesce (
+		min(message.sent_at),
+		strftime('%FT%R:%f+00:00', 'now', 'utc')
+	),
+	login.id
+from login
+left join message
+	on login.id = message.sender;
+
+insert into unsequenced (at, channel)
+select created_at, id
+from channel;
+
+insert into unsequenced (at, message)
+select sent_at, id
+from message;
+
+create table event (
+	at text
+		not null,
+	sequence
+		primary key
+		not null,
+	login text
+		references login (id),
+	channel text
+		references channel (id),
+	message text
+		references message (id),
+	check (
+		(login is not null and channel is null and message is null)
+		or (login is null and channel is not null and message is null)
+		or (login is null and channel is null and message is not null)
+	)
+);
+
+insert into event (at, sequence, login, channel, message)
+select
+	at, row_number() over (order by at, login is null, message is null), login, channel, message
+from unsequenced;
+
+-- Get this out of memory.
+drop table unsequenced;
+
+-- Because of how foundational `login` is, we pretty much have to recreate the
+-- whole schema.
+alter table message
+rename to old_message;
+
+alter table channel
+rename to old_channel;
+
+alter table token
+rename to old_token;
+
+alter table login
+rename to old_login;
+
+create table login (
+	id text
+		not null
+		primary key,
+	name text
+		not null
+		unique,
+	password_hash text
+		not null,
+	created_sequence bigint
+		unique
+		not null,
+	created_at text
+		not null
+);
+
+create table token (
+	id text
+		not null
+		primary key,
+	secret text
+		not null
+		unique,
+	login text
+		not null
+		references login (id),
+	issued_at text
+		not null,
+	last_used_at text
+		not null
+);
+
+create table channel (
+	id text
+		not null
+		primary key,
+	name text
+		unique
+		not null,
+	created_sequence bigint
+		unique
+		not null,
+	created_at text
+		not null
+);
+
+create table message (
+	id text
+		not null
+		primary key,
+	channel text
+		not null
+		references channel (id),
+	sender text
+		not null
+		references login (id),
+	sent_sequence bigint
+		unique
+		not null,
+	sent_at text
+		not null,
+	body text
+		not null
+);
+
+-- Copy data from the original tables, assigning sequence numbers as we go.
+insert into login (id, name, password_hash, created_sequence, created_at)
+select
+	old.id,
+	old.name,
+	old.password_hash,
+	event.sequence,
+	event.at
+from old_login as old
+join event on old.id = event.login
+order by event.sequence;
+
+insert into token (id, secret, login, issued_at, last_used_at)
+select id, secret, login, issued_at, last_used_at from old_token;
+
+insert into channel (id, name, created_sequence, created_at)
+select
+	old.id,
+	old.name,
+	event.sequence,
+	old.created_at
+from old_channel as old
+join event on old.id = event.channel
+order by event.sequence;
+
+insert into message (id, channel, sender, sent_sequence, sent_at, body)
+select
+	old.id,
+	old.channel,
+	old.sender,
+	event.sequence,
+	old.sent_at,
+	old.body
+from old_message as old
+join event on old.id = event.message
+order by event.sequence;
+
+-- Restart the event sequence, using the highest sequence number in the new
+-- event series.
+update event_sequence
+set last_value = (select coalesce(max(sequence), 0) from event);
+
+-- Clean up the now-unused original tables, plus the resequencing temp table
+drop table event;
+drop table old_message;
+drop table old_channel;
+drop table old_token;
+drop table old_login;
+
+-- Reindex, now that the original indices are no longer in the way
+create index token_issued_at
+on token (issued_at);
+
+create index message_sent_at
+on message (sent_at);
+
+create index channel_created_at
+on channel (created_at);
-- 
cgit v1.2.3

