Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions db/migrations/001_init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@
-- stays intact so that *post-launch* schema changes can be added as
-- new migration files.
--
-- Reviewer / automated-review guidance:
-- Until v1 ships, this file is the schema. Any rename, column
-- addition, or constraint change that lands BEFORE launch belongs
-- here, edited in place. Do NOT propose a forward ALTER TABLE
-- migration as a "safer" alternative — there is no deployed
-- database to migrate from, so a forward migration would only add
-- file-count noise while describing a history that never existed.
-- Once v1 is in production, this rule inverts and every schema
-- change becomes a new 002+ file.
--
-- Table overview
-- --------------
-- users
Expand Down Expand Up @@ -117,13 +127,24 @@ CREATE INDEX idx_tracked_mn_user ON tracked_masternodes(user_id);
CREATE INDEX idx_tracked_mn_outpoint
ON tracked_masternodes(collateral_txid, collateral_vout);

-- vote_reminder_log
-- Idempotency table for the reminder dispatcher. One row per
-- (user, governance cycle, bucket) — NOT per individual proposal —
-- because a single cycle bundles every proposal sharing a closing
-- window, and the product rule is "at most one reminder per cycle
-- per bucket regardless of proposal count". The dispatcher writes
-- a cycle identifier (e.g. `cycle:<voting_deadline_unix>`) into
-- scope_key; the UNIQUE constraint is what makes the tick
-- replay-safe. Column is `scope_key` from day one (not renamed
-- from `proposal_hash`) — see the pre-launch editing rule in the
-- header of this file.
CREATE TABLE vote_reminder_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
proposal_hash TEXT NOT NULL,
scope_key TEXT NOT NULL,
bucket TEXT NOT NULL,
sent_at INTEGER NOT NULL,
UNIQUE(user_id, proposal_hash, bucket)
UNIQUE(user_id, scope_key, bucket)
);

CREATE INDEX idx_vote_reminder_sent ON vote_reminder_log(sent_at);
Expand Down
1 change: 1 addition & 0 deletions lib/appFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ function mountAuthAndVault(
users: services.users,
sessions: services.sessions,
pendingRegistrations: services.pendingRegistrations,
vaults: services.vaults,
mailer,
sessionMw: services.sessionMw,
csrfMw: services.csrfMw,
Expand Down
15 changes: 10 additions & 5 deletions lib/db.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('db.openDatabase', () => {
db.close();
});

test('vote_reminder_log enforces one row per (user, proposal, bucket)', () => {
test('vote_reminder_log enforces one row per (user, scope_key, bucket)', () => {
const db = openDatabase(':memory:');
const now = Date.now();
const r = db
Expand All @@ -86,11 +86,16 @@ describe('db.openDatabase', () => {
.run('a@b.com', 'hash', FAKE_SALT_V, now, now);
const uid = r.lastInsertRowid;
const ins = db.prepare(
'INSERT INTO vote_reminder_log (user_id, proposal_hash, bucket, sent_at) VALUES (?, ?, ?, ?)'
'INSERT INTO vote_reminder_log (user_id, scope_key, bucket, sent_at) VALUES (?, ?, ?, ?)'
);
ins.run(uid, 'prop1', '1w', now);
ins.run(uid, 'prop1', '3d', now);
expect(() => ins.run(uid, 'prop1', '1w', now)).toThrow(/UNIQUE/i);
// Same scope_key, different buckets — allowed. Same scope_key +
// same bucket is a collision by design (the dispatcher would be
// re-sending what it already sent).
ins.run(uid, 'cycle:1700000000', 'days_before', now);
ins.run(uid, 'cycle:1700000000', 'final_24h', now);
expect(() =>
ins.run(uid, 'cycle:1700000000', 'days_before', now)
).toThrow(/UNIQUE/i);
db.close();
});

Expand Down
Loading