use cratefield_core::{Database, DbError, Row, Statement};
use sea_query::{Alias, Expr, Func, Query, SimpleExpr};
pub(crate) const STATUS_PENDING: &str = "pending";
pub(crate) const STATUS_CONFIRMED: &str = "confirmed";
fn iden(name: &str) -> Alias {
Alias::new(name)
}
fn select_all() -> sea_query::SelectStatement {
let mut select = Query::select();
select
.columns([
"id",
"email",
"email_normalized",
"product",
"status",
"generation",
"position",
"referral_code",
"referred_by",
"referrals",
"answers",
"created_at",
"confirmed_at",
])
.from(iden("waitlist_entries"));
select
}
#[derive(Debug, Clone)]
pub(crate) struct WaitlistRow {
pub id: String,
pub email: String,
pub email_normalized: String,
pub product: String,
pub status: String,
pub generation: i64,
pub position: Option<i64>,
pub referral_code: Option<String>,
pub referred_by: Option<String>,
pub referrals: i64,
pub answers: Option<String>,
pub created_at: String,
pub confirmed_at: Option<String>,
}
fn row_from(row: &Row) -> WaitlistRow {
WaitlistRow {
id: row.get::<String>("id").unwrap_or_default(),
email: row.get::<String>("email").unwrap_or_default(),
email_normalized: row.get::<String>("email_normalized").unwrap_or_default(),
product: row.get::<String>("product").unwrap_or_default(),
status: row.get::<String>("status").unwrap_or_default(),
generation: row.get::<i64>("generation").unwrap_or(1),
position: row.get::<i64>("position"),
referral_code: row.get::<Option<String>>("referral_code").flatten(),
referred_by: row.get::<Option<String>>("referred_by").flatten(),
referrals: row.get::<i64>("referrals").unwrap_or(0),
answers: row.get::<Option<String>>("answers").flatten(),
created_at: row.get::<String>("created_at").unwrap_or_default(),
confirmed_at: row.get::<Option<String>>("confirmed_at").flatten(),
}
}
pub(crate) async fn find_by_id(
db: &dyn Database,
id: &str,
) -> Result<Option<WaitlistRow>, DbError> {
let query = select_all()
.and_where(Expr::col(iden("id")).eq(id))
.limit(1)
.to_owned();
let rows = db.query(&Statement::render(&query)).await?;
Ok(rows.first().map(row_from))
}
pub(crate) async fn find_by_email_and_product(
db: &dyn Database,
email_normalized: &str,
product: &str,
) -> Result<Option<WaitlistRow>, DbError> {
let query = select_all()
.and_where(Expr::col(iden("email_normalized")).eq(email_normalized))
.and_where(Expr::col(iden("product")).eq(product))
.limit(1)
.to_owned();
let rows = db.query(&Statement::render(&query)).await?;
Ok(rows.first().map(row_from))
}
pub(crate) async fn find_confirmed_by_referral_code(
db: &dyn Database,
product: &str,
referral_code: &str,
) -> Result<Option<WaitlistRow>, DbError> {
let query = select_all()
.and_where(Expr::col(iden("product")).eq(product))
.and_where(Expr::col(iden("referral_code")).eq(referral_code))
.and_where(Expr::col(iden("status")).eq(STATUS_CONFIRMED))
.limit(1)
.to_owned();
let rows = db.query(&Statement::render(&query)).await?;
Ok(rows.first().map(row_from))
}
pub(crate) async fn insert_row(db: &dyn Database, row: &WaitlistRow) -> Result<(), DbError> {
let mut insert = Query::insert();
insert
.into_table(iden("waitlist_entries"))
.columns([
"id",
"email",
"email_normalized",
"product",
"status",
"generation",
"position",
"referral_code",
"referred_by",
"referrals",
"answers",
"created_at",
"confirmed_at",
])
.values_panic([
row.id.clone().into(),
row.email.clone().into(),
row.email_normalized.clone().into(),
row.product.clone().into(),
row.status.as_str().into(),
row.generation.into(),
row.position.into(),
row.referral_code.clone().into(),
row.referred_by.clone().into(),
row.referrals.into(),
row.answers.clone().into(),
row.created_at.clone().into(),
row.confirmed_at.clone().into(),
])
.on_conflict(
sea_query::OnConflict::columns([iden("email_normalized"), iden("product")])
.do_nothing()
.to_owned(),
);
db.execute(&Statement::render(&insert)).await?;
Ok(())
}
pub(crate) async fn refresh_pending(
db: &dyn Database,
id: &str,
answers: Option<&str>,
referred_by: Option<&str>,
now: &str,
) -> Result<u64, DbError> {
let generation = SimpleExpr::Case(Box::new(
Expr::case(
Expr::col(iden("status")).ne(STATUS_PENDING),
Expr::col(iden("generation")).add(1),
)
.finally(Expr::col(iden("generation"))),
));
let mut update = Query::update();
update
.table(iden("waitlist_entries"))
.values([
(iden("status"), STATUS_PENDING.into()),
(iden("generation"), generation),
(iden("answers"), answers.into()),
(iden("referred_by"), referred_by.into()),
(iden("created_at"), now.into()),
])
.and_where(Expr::col(iden("id")).eq(id))
.and_where(Expr::col(iden("status")).ne(STATUS_CONFIRMED));
db.execute(&Statement::render(&update)).await
}
fn allocated_position_expr(product: &str) -> SimpleExpr {
let mut counter = Query::select();
counter
.expr(Expr::col(iden("next_position")))
.from(iden("waitlist_position_lock"))
.and_where(Expr::col(iden("product")).eq(product))
.limit(1);
let coalesced = Func::coalesce([
SimpleExpr::SubQuery(None, Box::new(counter.into_sub_query_statement())),
0i64.into(),
]);
Expr::expr(coalesced).into()
}
pub(crate) async fn confirm_entry(
db: &dyn Database,
row: &WaitlistRow,
generation: i64,
now: &str,
referral_code: &str,
) -> Result<bool, DbError> {
let mut ensure_lock_row = Query::insert();
ensure_lock_row
.into_table(iden("waitlist_position_lock"))
.columns(["product", "updated_at"])
.values_panic([row.product.clone().into(), now.to_owned().into()])
.on_conflict(
sea_query::OnConflict::column(iden("product"))
.do_nothing()
.to_owned(),
);
let mut take_lock = Query::update();
take_lock
.table(iden("waitlist_position_lock"))
.value(iden("updated_at"), now)
.value(
iden("next_position"),
Expr::col(iden("next_position")).add(1),
)
.and_where(Expr::col(iden("product")).eq(row.product.as_str()));
let mut flip = Query::update();
flip.table(iden("waitlist_entries"))
.values([
(iden("status"), STATUS_CONFIRMED.into()),
(iden("confirmed_at"), now.into()),
(iden("position"), allocated_position_expr(&row.product)),
(iden("referral_code"), referral_code.into()),
])
.and_where(Expr::col(iden("id")).eq(row.id.as_str()))
.and_where(Expr::col(iden("status")).eq(STATUS_PENDING))
.and_where(Expr::col(iden("generation")).eq(generation));
let mut stmts = vec![
Statement::render(&ensure_lock_row),
Statement::render(&take_lock),
];
if let Some(referrer) = row.referred_by.as_deref() {
let mut still_pending = Query::select();
still_pending
.expr(Expr::val(1))
.from(iden("waitlist_entries"))
.and_where(Expr::col(iden("id")).eq(row.id.as_str()))
.and_where(Expr::col(iden("status")).eq(STATUS_PENDING))
.and_where(Expr::col(iden("generation")).eq(generation));
let mut credit = Query::update();
credit
.table(iden("waitlist_entries"))
.value(iden("referrals"), Expr::col(iden("referrals")).add(1))
.and_where(Expr::col(iden("id")).eq(referrer))
.and_where(Expr::exists(still_pending));
stmts.push(Statement::render(&credit));
}
stmts.push(Statement::render(&flip));
db.batch_atomic(&stmts).await?;
Ok(find_by_id(db, &row.id)
.await?
.is_some_and(|fresh| fresh.referral_code.as_deref() == Some(referral_code)))
}
pub(crate) async fn list_for_export(
db: &dyn Database,
product: Option<&str>,
fetch: u64,
offset: u64,
) -> Result<Vec<WaitlistRow>, DbError> {
let mut query = select_all();
query.order_by(iden("created_at"), sea_query::Order::Asc);
query.order_by(iden("id"), sea_query::Order::Asc);
if let Some(product) = product {
query.and_where(Expr::col(iden("product")).eq(product));
}
query.limit(fetch);
query.offset(offset);
let rows = db.query(&Statement::render(&query)).await?;
Ok(rows.rows.iter().map(row_from).collect())
}
pub(crate) async fn purge_pending_older_than(
db: &dyn Database,
cutoff_iso: &str,
) -> Result<u64, DbError> {
let mut delete = Query::delete();
delete
.from_table(iden("waitlist_entries"))
.and_where(Expr::col(iden("status")).eq(STATUS_PENDING))
.and_where(Expr::col(iden("created_at")).lt(cutoff_iso));
db.execute(&Statement::render(&delete)).await
}
#[cfg(test)]
mod tests {
use super::*;
use cratefield_core::{Module, Statement};
#[test]
fn replayed_confirm_credits_the_referrer_once() {
let module = crate::Waitlist::new().products(["kontinuum"]);
let db = cratefield_adapter_sqlite::SqliteDatabase::in_memory().expect("db");
db.apply_migrations(module.name(), module.migrations().sqlite)
.expect("migrate");
let insert = |id: &str, email: &str, status: &str, referred_by: Option<&str>| {
let stmt = Statement::with_values(
"INSERT INTO waitlist_entries \
(id, email, email_normalized, product, status, referrals, created_at, referred_by) \
VALUES (?, ?, ?, 'kontinuum', ?, 0, '2026-01-01T00:00:00Z', ?)",
vec![
id.into(),
email.into(),
email.into(),
status.into(),
referred_by.into(),
],
);
pollster::block_on(db.execute(&stmt)).expect("insert");
};
insert("referrer", "ref@example.com", "confirmed", None);
insert("friend", "friend@example.com", "pending", Some("referrer"));
let snapshot = pollster::block_on(find_by_id(&db, "friend"))
.expect("query")
.expect("row");
assert_eq!(snapshot.generation, 1);
let stale = pollster::block_on(confirm_entry(
&db,
&snapshot,
snapshot.generation + 1,
"2026-01-01T00:00:01Z",
"CODE1234",
))
.expect("confirm");
assert!(!stale, "a token from another generation flips nothing");
for _ in 0..2 {
pollster::block_on(confirm_entry(
&db,
&snapshot,
snapshot.generation,
"2026-01-01T00:00:01Z",
"CODE1234",
))
.expect("confirm");
}
let rows = pollster::block_on(db.query(&Statement::with_values(
"SELECT referrals FROM waitlist_entries WHERE id = ?",
vec!["referrer".into()],
)))
.expect("select");
let referrals = rows
.first()
.and_then(|row| row.get::<i64>("referrals"))
.expect("referrals");
assert_eq!(referrals, 1, "one referral, one credit");
}
#[test]
fn a_replayed_or_concurrent_confirm_reports_flipped_once() {
use cratefield_core::Module;
use std::sync::{Arc, Barrier};
let module = crate::Waitlist::new().products(["kontinuum"]);
let db = cratefield_adapter_sqlite::SqliteDatabase::in_memory().expect("db");
db.apply_migrations(module.name(), module.migrations().sqlite)
.expect("migrate");
pollster::block_on(db.execute(&Statement::with_values(
"INSERT INTO waitlist_entries \
(id, email, email_normalized, product, status, referrals, created_at) \
VALUES ('e1', 'a@b.test', 'a@b.test', 'kontinuum', 'pending', 0, \
'2026-01-01T00:00:00Z')",
vec![],
)))
.expect("insert");
let row = pollster::block_on(find_by_id(&db, "e1"))
.expect("query")
.expect("row");
let db = Arc::new(db);
let gate = Arc::new(Barrier::new(2));
let racers: Vec<_> = ["CODEAAAA", "CODEBBBB"]
.into_iter()
.map(|code| {
let db = Arc::clone(&db);
let gate = Arc::clone(&gate);
let row = row.clone();
std::thread::spawn(move || {
gate.wait();
pollster::block_on(confirm_entry(
&*db,
&row,
row.generation,
"2026-01-01T00:00:01Z",
code,
))
.expect("confirm")
})
})
.collect();
let flipped: Vec<bool> = racers
.into_iter()
.map(|racer| racer.join().expect("confirm thread"))
.collect();
assert_eq!(
flipped.iter().filter(|won| **won).count(),
1,
"exactly one confirm of one link reports a flip, not both: {flipped:?}"
);
let replay = pollster::block_on(confirm_entry(
&*db,
&row,
row.generation,
"2026-01-01T00:00:02Z",
"CODECCCC",
))
.expect("replay");
assert!(!replay, "a replayed confirm flips nothing");
}
#[test]
fn confirm_after_upgrade_continues_the_pre_0004_sequence() {
use crate::{
MIGRATION_ANONYMISABLE_ENTRY, MIGRATION_ENTRY_GENERATION, MIGRATION_INIT,
MIGRATION_LOCK_BACKFILL, MIGRATION_MAIL_COOLDOWN, MIGRATION_POSITION_COUNTER,
MIGRATION_POSITION_LOCK,
};
let module = crate::Waitlist::new().products(["kontinuum"]);
let db = cratefield_adapter_sqlite::SqliteDatabase::in_memory().expect("db");
db.apply_migrations(
module.name(),
&[
MIGRATION_INIT,
MIGRATION_ENTRY_GENERATION,
MIGRATION_MAIL_COOLDOWN,
MIGRATION_POSITION_LOCK,
],
)
.expect("migrate to 0004");
let seed = |id: &str, email: &str, status: &str, position: Option<i64>| {
let confirmed_at = (position.is_some()).then_some("2026-01-01T00:00:00Z");
let stmt = Statement::with_values(
"INSERT INTO waitlist_entries \
(id, email, email_normalized, product, status, position, \
referrals, created_at, confirmed_at, generation) \
VALUES (?, ?, ?, 'kontinuum', ?, ?, 0, '2026-01-01T00:00:00Z', ?, 1)",
vec![
id.into(),
email.into(),
email.into(),
status.into(),
position.into(),
confirmed_at.into(),
],
);
pollster::block_on(db.execute(&stmt)).expect("insert");
};
seed("old-1", "one@example.com", "confirmed", Some(1));
seed("old-2", "two@example.com", "confirmed", Some(2));
let locks = pollster::block_on(db.query(&Statement::new(
"SELECT product FROM waitlist_position_lock",
)))
.expect("select");
assert!(
locks.is_empty(),
"a pre-0004 database has confirmed positions and no lock row"
);
db.apply_migrations(
module.name(),
&[
MIGRATION_ANONYMISABLE_ENTRY,
MIGRATION_POSITION_COUNTER,
MIGRATION_LOCK_BACKFILL,
],
)
.expect("migrate to 0007");
seed("newcomer", "three@example.com", "pending", None);
let snapshot = pollster::block_on(find_by_id(&db, "newcomer"))
.expect("query")
.expect("row");
assert_eq!(snapshot.position, None, "pending entries hold no position");
let flipped = pollster::block_on(confirm_entry(
&db,
&snapshot,
snapshot.generation,
"2026-01-01T00:00:01Z",
"CODE1234",
))
.expect("confirm after upgrade");
assert!(flipped, "a pending entry at its generation confirms");
let confirmed = pollster::block_on(find_by_id(&db, "newcomer"))
.expect("query")
.expect("row");
assert_eq!(
confirmed.position,
Some(3),
"the counter continues the pre-0004 sequence, not position 1"
);
let counters = pollster::block_on(db.query(&Statement::new(
"SELECT next_position FROM waitlist_position_lock WHERE product = 'kontinuum'",
)))
.expect("select");
assert_eq!(
counters
.first()
.and_then(|row| row.get::<i64>("next_position")),
Some(3),
"0007 seeded the counter at 2 and the confirm advanced it to 3"
);
}
}