use crate::envelope;
use crate::wire::Codec;
#[cfg(feature = "sqlite")]
use {
crate::dialect::{ident, literal},
crate::envelope::Up,
crate::envelope::{framed, Body, Down, EndSnapshot, Table, ToReplica},
crate::row::{key_text, Key, KeyKind},
crate::{Conn, Error},
prost::Message as _,
std::collections::{HashMap, HashSet},
uuid::Uuid,
};
pub(crate) fn codec_of<'a, P>(codecs: &'a [Codec<P>], table: &str) -> Option<&'a Codec<P>> {
codecs.iter().find(|c| c.table.name == table)
}
pub(crate) fn keys_of<P: Sync + 'static>(
codec: &Codec<P>,
wire: &[envelope::Key],
) -> Vec<crate::row::Key> {
parsed(codec, wire).into_iter().flatten().collect()
}
pub(crate) fn parsed<P: Sync + 'static>(
codec: &Codec<P>,
wire: &[envelope::Key],
) -> Vec<Option<crate::row::Key>> {
wire.iter()
.map(|key| {
codec
.key(&key.parts)
.inspect_err(|e| {
tracing::error!(
table = codec.table.name,
"sync: a key that does not parse — left out: {}",
capped(&e.to_string())
);
})
.ok()
})
.collect()
}
#[cfg(feature = "sqlite")]
const MIGRATIONS: [&str; crate::REPLICA_SCHEMA_VERSION as usize] = [
include_str!("../sql/sqlite/1.sql"),
include_str!("../sql/sqlite/2.sql"),
];
#[cfg(feature = "sqlite")]
#[must_use]
pub fn migration(version: i32) -> Option<String> {
let step = MIGRATIONS.get(usize::try_from(version.checked_sub(1)?).ok()?)?;
Some(format!(
"{step}\nINSERT INTO sync_version (id, version) VALUES (1, {version}) \
ON CONFLICT (id) DO UPDATE SET version = excluded.version;\n"
))
}
#[cfg(feature = "sqlite")]
struct Trigger {
name: String,
sql: String,
}
#[cfg(feature = "sqlite")]
fn generated<P: Sync + 'static>(codecs: &[&Codec<P>]) -> Vec<Trigger> {
use crate::row::Mirror;
const QUIET: &str = " WHEN NOT EXISTS (SELECT 1 FROM sync_applying)";
const NOTED: &str =
"ON CONFLICT (relation, row_key) DO UPDATE SET writes = writes + 1, set_aside = NULL";
let mut out = Vec::new();
for codec in codecs {
let table = codec.table.name;
let on = ident(table);
let key_columns = key_columns(&codec.table);
let key = |row: &str| key_sql(&codec.table, row);
let readable = key_columns
.iter()
.map(|c| {
let value = format!("NEW.{}", ident(c.name));
if c.key == Some(KeyKind::Integer) {
format!("typeof({value}) = 'integer'")
} else {
format!("(typeof({value}) = 'blob' AND length({value}) = 16)")
}
})
.collect::<Vec<_>>()
.join(" AND ");
let guard = format!(
"SELECT RAISE(ABORT, {}) WHERE NOT ({readable}); ",
literal(&format!(
"`{table}`: a key stored in a form sync does not read — a uuid is a 16-byte blob, an integer an integer"
))
);
let note = |row: &str| {
format!(
"INSERT INTO sync_notes (relation, row_key) VALUES ({}, {}) {NOTED}; ",
literal(table),
key(row)
)
};
let gone = format!(
"INSERT INTO sync_notes (relation, row_key) SELECT {}, {} WHERE {} IS NOT {} {NOTED}; ",
literal(table),
key("OLD"),
key("OLD"),
key("NEW")
);
let refuse = |event: &str, when: &str, why: String| Trigger {
name: format!("sync_refuse_{table}_{}", event.to_lowercase()),
sql: format!(
"CREATE TRIGGER {} BEFORE {event} ON {on}{when} BEGIN SELECT RAISE(ABORT, {}); END",
ident(&format!("sync_refuse_{table}_{}", event.to_lowercase())),
literal(&why)
),
};
let noting = |event: &str, of: &str, when: &str, body: String| Trigger {
name: format!("sync_note_{table}_{}", event.to_lowercase()),
sql: format!(
"CREATE TRIGGER {} AFTER {event}{of} ON {on}{when} BEGIN {body}END",
ident(&format!("sync_note_{table}_{}", event.to_lowercase()))
),
};
match codec.table.mirror {
Mirror::Catalog => {
for event in ["INSERT", "UPDATE", "DELETE"] {
let why = format!("`{table}` is the server's: a replica does not write it");
out.push(refuse(event, QUIET, why));
}
}
Mirror::Record | Mirror::Setting => {
out.push(noting("INSERT", "", "", format!("{guard}{}", note("NEW"))));
if codec.table.columns.iter().any(|c| c.version.is_some()) {
let body = format!("{guard}{}{gone}", note("NEW"));
out.push(noting("UPDATE", "", "", body));
} else {
let why = format!(
"`{table}` is history: a row of it is written once (a version column makes it editable)"
);
out.push(refuse("UPDATE", "", why));
}
out.push(noting("DELETE", "", "", note("OLD")));
}
Mirror::Both => {
let why =
format!("a row of `{table}` begins on the server: a replica only edits it");
out.push(refuse("INSERT", QUIET, why));
let (owned, theirs): (Vec<&crate::row::Column>, Vec<&crate::row::Column>) =
codec.table.columns.iter().partition(|c| c.replica_owned);
let changed = theirs
.iter()
.map(|c| format!("OLD.{0} IS NOT NEW.{0}", ident(c.name)))
.collect::<Vec<_>>()
.join(" OR ");
let why = format!(
"`{table}` is the server's but for the columns a replica owns: a replica \
edits only those"
);
out.push(refuse("UPDATE", &format!("{QUIET} AND ({changed})"), why));
let of = if owned.is_empty() {
String::new()
} else {
let owned: Vec<String> = owned.iter().map(|c| ident(c.name)).collect();
format!(" OF {}", owned.join(", "))
};
let body = format!("{guard}{}{gone}", note("NEW"));
out.push(noting("UPDATE", &of, QUIET, body));
if let Some(origin) = codec.table.origin.filter(|_| codec.takes_deletes()) {
let why = format!(
"a row of `{table}` is let go of by the server: a replica deletes only \
the ones keyed by it, once it knows which it is"
);
let others = format!(
"{QUIET} AND NOT EXISTS (SELECT 1 FROM sync_replica WHERE replica = OLD.{})",
ident(origin)
);
out.push(refuse("DELETE", &others, why));
out.push(noting("DELETE", "", QUIET, note("OLD")));
} else {
let why = format!(
"a row of `{table}` is let go of by the server: a replica deletes one \
only of a table keyed by it"
);
out.push(refuse("DELETE", QUIET, why));
}
}
}
}
out
}
#[cfg(feature = "sqlite")]
fn key_columns(table: &crate::row::Table) -> Vec<&crate::row::Column> {
table
.key
.iter()
.filter_map(|k| table.columns.iter().find(|c| c.name == *k))
.collect()
}
#[cfg(feature = "sqlite")]
fn key_sql(table: &crate::row::Table, row: &str) -> String {
key_columns(table)
.iter()
.map(|c| {
let value = format!("{row}.{}", ident(c.name));
if c.key == Some(KeyKind::Integer) {
format!("CAST({value} AS TEXT)")
} else {
let hex = format!("hex({value})");
format!(
"lower(substr({hex}, 1, 8) || '-' || substr({hex}, 9, 4) || '-' || \
substr({hex}, 13, 4) || '-' || substr({hex}, 17, 4) || '-' || substr({hex}, 21))"
)
}
})
.collect::<Vec<_>>()
.join(" || '|' || ")
}
#[cfg(feature = "sqlite")]
async fn restate<P: Sync + 'static>(conn: &mut Conn, codecs: &[Codec<P>]) -> Result<(), Error> {
for codec in codecs {
let table = &codec.table;
let only = match table.mirror {
crate::row::Mirror::Setting => " WHERE true",
_ if table.name == crate::wish::TABLE => {
" WHERE t.answered IS NOT NULL AND t.retired_at IS NULL"
}
_ => continue,
};
sqlx::query(&format!(
"INSERT INTO sync_notes (relation, row_key, restated) SELECT {}, {}, 1 FROM {} t{only} \
ON CONFLICT (relation, row_key) DO UPDATE SET writes = writes + 1, restated = 1, \
upload = NULL, sent = NULL, sent_at = NULL",
literal(table.name),
key_sql(table, "t"),
ident(table.name)
))
.execute(&mut *conn)
.await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
async fn gone_here<P: Sync + 'static>(
conn: &mut Conn,
codec: &Codec<P>,
recorded: &[Key],
) -> Result<(), Error> {
let held = codec.present(conn, recorded).await?;
for key in recorded.iter().filter(|k| !held.contains(k)) {
sqlx::query(
"INSERT INTO sync_notes (relation, row_key) VALUES ($1, $2) \
ON CONFLICT (relation, row_key) DO UPDATE SET writes = writes + 1",
)
.bind(codec.table.name)
.bind(key_text(key))
.execute(&mut *conn)
.await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
pub fn triggers<P: Sync + 'static>(codecs: &[Codec<P>]) -> Result<String, Error> {
let wishes = crate::wish::engines(codecs)?;
Ok(generated(&codecs.iter().chain(&wishes).collect::<Vec<_>>())
.into_iter()
.map(|t| format!("DROP TRIGGER IF EXISTS {};\n{};\n", ident(&t.name), t.sql))
.collect())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn identified(conn: &mut Conn, replica: Uuid) -> Result<(), Error> {
sqlx::query(
"INSERT INTO sync_replica (id, replica) VALUES (1, $1) \
ON CONFLICT (id) DO UPDATE SET replica = EXCLUDED.replica \
WHERE replica IS NOT EXCLUDED.replica",
)
.bind(replica)
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
async fn schema_version(conn: &mut Conn) -> Result<i32, Error> {
let versioned: bool = sqlx::query_scalar(
"SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sync_version')",
)
.fetch_one(&mut *conn)
.await?;
if !versioned {
return Ok(0);
}
Ok(sqlx::query_scalar("SELECT version FROM sync_version")
.fetch_optional(&mut *conn)
.await?
.unwrap_or(0))
}
#[cfg(feature = "sqlite")]
pub async fn check<P: Sync + 'static>(conn: &mut Conn, codecs: &[Codec<P>]) -> Result<(), Error> {
let wishes = crate::wish::engines(codecs)?;
checked(conn, &codecs.iter().chain(&wishes).collect::<Vec<_>>()).await
}
#[cfg(feature = "sqlite")]
pub(crate) async fn checked<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[&Codec<P>],
) -> Result<(), Error> {
let (version, needed) = (schema_version(conn).await?, crate::REPLICA_SCHEMA_VERSION);
if version > needed {
return Err(Error::Misuse(format!(
"the engine's tables are version {version}, from a later build than this one \
({needed}): run that build, or a later one"
)));
}
if version < needed {
return Err(Error::Misuse(format!(
"the engine's tables are version {version}, and this build needs {needed}: \
`replica::install` brings them there, or a migration of yours runs the \
engine's steps (the book's \"Upgrading\")"
)));
}
let tables: Vec<&crate::row::Table> = codecs.iter().map(|c| &c.table).collect();
crate::row::parity(conn, &tables).await?;
for table in &tables {
let Some(retired) = table.retired else {
continue;
};
for (index, sql) in crate::dialect::unique_indexes(conn, table.name).await? {
if !sql.is_some_and(|sql| leaves_out(&sql, retired)) {
return Err(Error::Misuse(format!(
"`{}`: the unique index `{index}` holds its retired rows' values too, which \
would refuse every row the server sends with one of them while the retired \
row stays — make it partial: `WHERE {retired} IS NULL`",
table.name
)));
}
}
}
let rows: Vec<(String, String, String)> =
sqlx::query_as("SELECT name, tbl_name, sql FROM sqlite_master WHERE type = 'trigger'")
.fetch_all(&mut *conn)
.await?;
let live: HashMap<String, String> = rows
.into_iter()
.filter(|(name, table, _)| !unknown(codecs, name, table))
.map(|(name, _, sql)| (name, sql))
.collect();
let normal = |sql: &str| sql.split_whitespace().collect::<Vec<_>>().join(" ");
let wanted = generated(codecs);
for trigger in &wanted {
match live.get(&trigger.name) {
None => {
return Err(Error::Misuse(format!(
"the trigger `{}` is missing: install, or run replica::triggers' script in a migration",
trigger.name
)))
}
Some(sql) if normal(sql) != normal(&trigger.sql) => {
return Err(Error::Misuse(format!(
"the trigger `{}` is not the one replica::triggers writes: {sql}",
trigger.name
)))
}
Some(_) => {}
}
}
if let Some(name) = live
.keys()
.find(|name| engines(name) && !wanted.iter().any(|t| t.name == **name))
{
return Err(Error::Misuse(format!(
"the trigger `{name}` is not one of the engine's: a table that no longer travels, or one written by hand — drop it"
)));
}
settled(conn).await?;
Ok(())
}
#[cfg(feature = "sqlite")]
fn leaves_out(sql: &str, retired: &str) -> bool {
let normal = |text: &str| {
text.to_lowercase()
.replace(['"', '`', '[', ']'], "")
.replace('(', " ( ")
.replace(')', " ) ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
};
let live = normal(&format!("{retired} is null"));
let sql = normal(sql);
let Some((_, filter)) = sql.rsplit_once(" where ") else {
return false;
};
let mut terms = vec![filter.to_owned()];
while let Some(term) = terms.pop() {
if term == live {
return true;
}
let words: Vec<&str> = term.split(' ').collect();
let mut depth = 0i32;
let mut parts = vec![Vec::new()];
let mut closed_at_end = true;
let mut either = false;
for (at, word) in words.iter().enumerate() {
match *word {
"(" => depth += 1,
")" => {
depth -= 1;
closed_at_end &= depth > 0 || at == words.len() - 1;
}
"and" if depth == 0 => {
parts.push(Vec::new());
continue;
}
"or" if depth == 0 => either = true,
_ => {}
}
parts.last_mut().expect("one at least").push(*word);
}
if either {
continue;
}
if parts.len() > 1 {
terms.extend(parts.into_iter().map(|p| p.join(" ")));
} else if words.first() == Some(&"(") && closed_at_end {
terms.push(words[1..words.len() - 1].join(" "));
}
}
false
}
#[cfg(feature = "sqlite")]
fn engines(name: &str) -> bool {
name.starts_with("sync_note_") || name.starts_with("sync_refuse_")
}
#[cfg(feature = "sqlite")]
fn unknown<P: Sync + 'static>(codecs: &[&Codec<P>], name: &str, table: &str) -> bool {
name.starts_with("sync_note_")
&& !codecs
.iter()
.any(|c| c.table.name.eq_ignore_ascii_case(table))
}
#[cfg(feature = "sqlite")]
pub(crate) async fn settled(conn: &mut Conn) -> Result<(), Error> {
let synchronous: i64 = sqlx::query_scalar("PRAGMA synchronous")
.fetch_one(&mut *conn)
.await?;
let journal: String = sqlx::query_scalar("PRAGMA journal_mode")
.fetch_one(&mut *conn)
.await?;
let file: String =
sqlx::query_scalar("SELECT file FROM pragma_database_list WHERE name = 'main'")
.fetch_one(&mut *conn)
.await?;
let recursive: i64 = sqlx::query_scalar("PRAGMA recursive_triggers")
.fetch_one(&mut *conn)
.await?;
let refused = if synchronous < 2 {
"its commits are not durable (`synchronous` below FULL): a message acknowledged could be lost"
} else if !file.is_empty() && ["off", "memory"].contains(&journal.to_lowercase().as_str()) {
"it cannot roll a transaction back after a crash (`journal_mode` OFF or MEMORY): a message could be half applied"
} else if recursive == 0 {
"its triggers miss the rows a conflict replaces (`recursive_triggers` off): a row an \
INSERT OR REPLACE deletes would never go up as gone — set it ON in the pool's options"
} else {
return Ok(());
};
Err(Error::Misuse(format!(
"the database's connection: {refused}"
)))
}
#[cfg(feature = "sqlite")]
pub async fn install<P: Sync + 'static>(conn: &mut Conn, codecs: &[Codec<P>]) -> Result<(), Error> {
let wishes = crate::wish::engines(codecs)?;
let all: &[&Codec<P>] = &codecs.iter().chain(&wishes).collect::<Vec<_>>();
let mut tx = sqlx::Connection::begin_with(&mut *conn, "BEGIN IMMEDIATE").await?;
let version = schema_version(&mut tx).await?;
if version > crate::REPLICA_SCHEMA_VERSION {
return Err(Error::Misuse(format!(
"the engine's tables are version {version}, later than this build's {}",
crate::REPLICA_SCHEMA_VERSION
)));
}
for step in version + 1..=crate::REPLICA_SCHEMA_VERSION {
let script = migration(step).expect("a step of this build");
sqlx::Executor::execute(&mut *tx, script.as_str()).await?;
}
let live: Vec<(String, String)> =
sqlx::query_as("SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'")
.fetch_all(&mut *tx)
.await?;
let ours = live
.into_iter()
.filter(|(name, table)| engines(name) && !unknown(all, name, table));
for (name, _) in ours {
sqlx::query(&format!("DROP TRIGGER {}", ident(&name)))
.execute(&mut *tx)
.await?;
}
for trigger in generated(all) {
sqlx::query(&trigger.sql).execute(&mut *tx).await?;
}
checked(&mut tx, all).await?;
tx.commit().await?;
Ok(())
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Applied {
pub table: String,
pub keys: Vec<Key>,
}
#[cfg(feature = "sqlite")]
#[derive(Debug)]
#[non_exhaustive]
pub enum Received {
Applied(Vec<Applied>),
SnapshotEnded,
Filed,
AlreadyApplied,
Gap,
Malformed(String),
}
#[cfg(feature = "sqlite")]
pub(crate) async fn receive<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
bytes: &[u8],
limits: &envelope::Limits,
care: &Care,
) -> Result<Received, Stop> {
let message = match ToReplica::decode(bytes) {
Ok(message) => message,
Err(e) => return Ok(malformed(format!("bytes that are no message: {e}"))),
};
if message.protocol > envelope::PROTOCOL {
return Err(Stop::Failed(Error::Protocol {
theirs: message.protocol,
ours: envelope::PROTOCOL,
}));
}
if message.protocol == 0 {
return Ok(malformed("a message that names no protocol".to_owned()));
}
let Some(body) = message.body else {
return Ok(malformed(
"a message with nothing this build knows in it".to_owned(),
));
};
if let Some(why) = envelope::oversized(&body, limits) {
return Ok(malformed(why));
}
sqlx::query("INSERT OR IGNORE INTO sync_applying (id) VALUES (1)")
.execute(&mut *conn)
.await?;
let received = if care.careful() {
carefully(conn, codecs, body, care).await
} else {
act(conn, codecs, body, care).await
};
let quiet = sqlx::query("DELETE FROM sync_applying")
.execute(&mut *conn)
.await;
let received = received?;
quiet?;
Ok(received)
}
#[cfg(feature = "sqlite")]
#[derive(Debug)]
pub(crate) enum Stop {
Failed(Error),
Culprits(Vec<Culprit>),
}
#[cfg(feature = "sqlite")]
impl From<Error> for Stop {
fn from(e: Error) -> Self {
Stop::Failed(e)
}
}
#[cfg(feature = "sqlite")]
impl From<sqlx::Error> for Stop {
fn from(e: sqlx::Error) -> Self {
Stop::Failed(e.into())
}
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Culprit {
table: String,
row: Spot,
why: String,
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Spot {
Upsert { entry: usize, at: usize },
LetGo(String),
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Default)]
pub(crate) struct Care {
level: Level,
culprits: HashMap<(String, Spot), Culprit>,
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Level {
#[default]
Bold,
Careful,
Whole,
}
#[cfg(feature = "sqlite")]
const WHOLE: &str =
"the database refused the message it came in, for what no row of it alone explains";
#[cfg(feature = "sqlite")]
impl Care {
fn careful(&self) -> bool {
self.level >= Level::Careful
}
pub(crate) fn heed(&mut self) -> bool {
let next = match self.level {
Level::Bold => Level::Careful,
Level::Careful => Level::Whole,
Level::Whole => return false,
};
self.level = next;
true
}
pub(crate) fn refuse(&mut self, culprits: Vec<Culprit>) -> bool {
self.level = self.level.max(Level::Careful);
let mut news = false;
for culprit in culprits {
let place = (culprit.table.clone(), culprit.row.clone());
if self.culprits.contains_key(&place) {
continue;
}
tracing::error!(
table = culprit.table,
"sync: a row the database would refuse the message for — applied again without it: {}",
culprit.why
);
self.culprits.insert(place, culprit);
news = true;
}
news
}
fn culprit(&self, table: &str, row: &Spot) -> Option<&str> {
if self.level == Level::Whole {
return Some(WHOLE);
}
self.culprits
.get(&(table.to_owned(), row.clone()))
.map(|c| c.why.as_str())
}
}
#[cfg(feature = "sqlite")]
const ROW: &str = "sync_row";
#[cfg(feature = "sqlite")]
async fn row_done<T>(
conn: &mut Conn,
result: Result<T, Stop>,
table: &str,
row: Spot,
) -> Result<Result<T, Error>, Stop> {
let refusal = match result {
Ok(value) => {
sqlx::query(&format!("RELEASE SAVEPOINT {ROW}"))
.execute(&mut *conn)
.await?;
return Ok(Ok(value));
}
Err(Stop::Failed(e)) if e.is_refusal() => e,
Err(stop) => return Err(stop),
};
if sqlx::query(&format!("ROLLBACK TO SAVEPOINT {ROW}"))
.execute(&mut *conn)
.await
.is_err()
{
return Err(Stop::Culprits(vec![Culprit {
table: table.to_owned(),
row,
why: refusal.to_string(),
}]));
}
sqlx::query(&format!("RELEASE SAVEPOINT {ROW}"))
.execute(&mut *conn)
.await?;
Ok(Err(refusal))
}
#[cfg(feature = "sqlite")]
async fn row_begun(conn: &mut Conn) -> Result<(), Error> {
sqlx::query(&format!("SAVEPOINT {ROW}"))
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) fn malformed(why: String) -> Received {
tracing::error!("sync: a message this build can do nothing with — dropped: {why}");
Received::Malformed(why)
}
#[cfg(feature = "sqlite")]
async fn carefully<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
body: Body,
care: &Care,
) -> Result<Received, Stop> {
let mut upserts: HashMap<(&str, Key), Spot> = HashMap::new();
if let Body::Down(down) = &body {
for (entry, table) in down.tables.iter().enumerate() {
let Some(codec) = codec_of(codecs, &table.table).filter(|c| c.sent_down()) else {
continue;
};
for (at, key) in parsed(codec, &table.keys).into_iter().enumerate() {
if let Some(key) = key {
upserts.insert((codec.table.name, key), Spot::Upsert { entry, at });
}
}
}
}
let mut before = Vec::new();
let mut recorded = 0;
for codec in codecs.iter().filter(|c| c.sent_down()) {
let pointing = codec.pointing(conn, recorded).await?;
recorded += pointing.len();
before.push((codec, pointing));
}
let received = act(conn, codecs, body, care).await?;
let mut culprits = Vec::new();
let why = "rows here point at what it changes, through a key checked at commit";
for (codec, pointing) in &before {
let table = codec.table.name;
let broken = codec.broken(conn, pointing).await?;
let held: HashSet<Key> = codec.present(conn, &broken).await?.into_iter().collect();
for key in broken {
let row = held
.contains(&key)
.then(|| upserts.remove(&(table, key.clone())))
.flatten()
.unwrap_or_else(|| Spot::LetGo(key_text(&key)));
culprits.push(Culprit {
table: table.to_owned(),
row,
why: why.to_owned(),
});
}
crate::row::forget(conn, pointing).await?;
if !culprits.is_empty() {
continue;
}
let mine: Vec<Key> = upserts
.keys()
.filter(|(t, _)| *t == table)
.map(|(_, key)| key.clone())
.collect();
for key in codec.orphaned(conn, &mine).await? {
let row = upserts.remove(&(table, key)).expect("one of the message's");
culprits.push(Culprit {
table: table.to_owned(),
row,
why: "a row it points at, through a foreign key, is not held here".to_owned(),
});
}
}
if culprits.is_empty() {
Ok(received)
} else {
Err(Stop::Culprits(culprits))
}
}
#[cfg(feature = "sqlite")]
async fn act<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
body: Body,
care: &Care,
) -> Result<Received, Stop> {
match body {
Body::Filed(message) => {
filed(conn, codecs, &message, care).await?;
Ok(Received::Filed)
}
Body::Down(down) => {
if let Some(table) = down.tables.iter().find(|t| t.keys.len() != t.upserts.len()) {
return Ok(malformed(format!(
"`{}`: {} rows and {} keys beside them",
capped(&table.table),
table.upserts.len(),
table.keys.len()
)));
}
let place = Place {
epoch: &down.epoch,
sequence: down.sequence,
nonce: down.nonce,
mac: &down.mac,
previous: (down.previous, down.previous_nonce),
snapshot: &down.snapshot_id,
ends: false,
};
match chained(conn, place).await? {
Ok(by) => {
if down.previous == 0 {
restate(conn, codecs).await?;
}
Ok(Received::Applied(
apply(conn, codecs, &down, &by, care).await?,
))
}
Err(skipped) => Ok(skipped),
}
}
Body::EndSnapshot(end) => {
let place = Place {
epoch: &end.epoch,
sequence: end.sequence,
nonce: end.nonce,
mac: &end.mac,
previous: (end.previous, end.previous_nonce),
snapshot: &end.snapshot_id,
ends: true,
};
match chained(conn, place).await? {
Ok(by) => {
if end.previous == 0 {
restate(conn, codecs).await?;
}
end_snapshot(conn, codecs, &end, &by, care).await?;
Ok(Received::SnapshotEnded)
}
Err(skipped) => Ok(skipped),
}
}
}
}
#[cfg(feature = "sqlite")]
struct Place<'m> {
epoch: &'m str,
sequence: u64,
nonce: u64,
mac: &'m [u8],
previous: (u64, u64),
snapshot: &'m str,
ends: bool,
}
#[cfg(feature = "sqlite")]
const SEALED: usize = 16;
#[cfg(feature = "sqlite")]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct By {
epoch: String,
sequence: i64,
}
#[cfg(feature = "sqlite")]
#[derive(sqlx::FromRow)]
struct Last {
epoch: String,
sequence: i64,
nonce: i64,
snapshot_id: Option<String>,
gap: i64,
}
#[cfg(feature = "sqlite")]
async fn chained(conn: &mut Conn, place: Place<'_>) -> Result<Result<By, Received>, Error> {
let Ok(sequence) = i64::try_from(place.sequence) else {
return Ok(Err(malformed(format!(
"sequence {}, outside 1 to 2^63 - 1",
place.sequence
))));
};
let Ok(epoch) = Uuid::parse_str(place.epoch).map(|e| e.to_string()) else {
return Ok(Err(malformed(format!(
"epoch `{}` is not a uuid",
capped(place.epoch)
))));
};
let snapshot = if place.snapshot.is_empty() {
Ok(String::new())
} else {
Uuid::parse_str(place.snapshot).map(|id| id.to_string())
};
let Ok(snapshot) = snapshot else {
return Ok(Err(malformed(format!(
"snapshot `{}` is not a uuid",
capped(place.snapshot)
))));
};
let Some(previous) = i64::try_from(place.previous.0)
.ok()
.filter(|p| *p < sequence)
else {
return Ok(Err(malformed(format!(
"message {sequence} follows {}, which is not before it",
place.previous.0
))));
};
let (Ok(nonce @ 1..), Ok(previous_nonce)) =
(i64::try_from(place.nonce), i64::try_from(place.previous.1))
else {
return Ok(Err(malformed(format!(
"message {sequence}: nonce {}, following nonce {}, outside 1 to 2^63 - 1",
place.nonce, place.previous.1
))));
};
if (previous == 0) != (previous_nonce == 0) {
return Ok(Err(malformed(format!(
"message {sequence} follows {previous} of nonce {previous_nonce}"
))));
}
if previous == 0 && snapshot.is_empty() {
return Ok(Err(malformed(
"a message that follows nothing and begins no snapshot".to_owned(),
)));
}
if place.mac.len() != SEALED {
return Ok(Err(malformed(format!(
"message {sequence} sealed with {} bytes, not {SEALED}",
place.mac.len()
))));
}
let last: Option<Last> = sqlx::query_as(
"SELECT epoch, sequence, nonce, snapshot_id, gap FROM sync_applied WHERE id = 1",
)
.fetch_optional(&mut *conn)
.await?;
if last
.as_ref()
.is_some_and(|l| l.epoch == epoch && sequence <= l.sequence)
{
tracing::info!(
sequence,
"sync: a message at or below the last applied — nothing to do"
);
sqlx::query("UPDATE sync_applied SET acknowledged = 0 WHERE id = 1")
.execute(&mut *conn)
.await?;
return Ok(Err(Received::AlreadyApplied));
}
if previous == 0 {
if last.as_ref().is_some_and(|l| l.epoch != epoch) {
sqlx::query("DELETE FROM sync_backlog")
.execute(&mut *conn)
.await?;
}
sqlx::query("DELETE FROM sync_snapshot_keys")
.execute(&mut *conn)
.await?;
sqlx::query(
"INSERT INTO sync_applied (id, epoch, sequence, nonce, mac, snapshot_id, snapshot_began) \
VALUES (1, $1, $2, $3, $4, $5, $2) \
ON CONFLICT (id) DO UPDATE SET epoch = EXCLUDED.epoch, sequence = EXCLUDED.sequence, \
nonce = EXCLUDED.nonce, mac = EXCLUDED.mac, \
snapshot_id = EXCLUDED.snapshot_id, snapshot_began = EXCLUDED.snapshot_began, \
gap = 0, gap_told = 0, \
acknowledged = CASE WHEN sync_applied.epoch = EXCLUDED.epoch \
THEN sync_applied.acknowledged ELSE 0 END",
)
.bind(&epoch)
.bind(sequence)
.bind(nonce)
.bind(place.mac)
.bind(&snapshot)
.execute(&mut *conn)
.await?;
return Ok(Ok(By { epoch, sequence }));
}
let follows = last.as_ref().is_some_and(|l| {
l.gap == 0 && l.epoch == epoch && l.sequence == previous && l.nonce == previous_nonce
});
let Some(Last { snapshot_id, .. }) = last.filter(|_| follows) else {
tracing::warn!(
sequence,
previous,
"sync: a message that follows one this replica never applied — it missed one, \
and waits to be provisioned again"
);
sqlx::query(
"INSERT INTO sync_applied (id, epoch, sequence, gap) VALUES (1, '', 0, $1) \
ON CONFLICT (id) DO UPDATE SET gap = max(gap, EXCLUDED.gap)",
)
.bind(sequence)
.execute(&mut *conn)
.await?;
return Ok(Err(Received::Gap));
};
let named = (!snapshot.is_empty() || place.ends).then_some(snapshot.as_str());
if named != snapshot_id.as_deref() {
return Ok(Err(malformed(format!(
"message {sequence} of snapshot `{}`, while `{}` is in progress",
snapshot,
snapshot_id.unwrap_or_default()
))));
}
sqlx::query("UPDATE sync_applied SET sequence = $1, nonce = $2, mac = $3 WHERE id = 1")
.bind(sequence)
.bind(nonce)
.bind(place.mac)
.execute(&mut *conn)
.await?;
Ok(Ok(By { epoch, sequence }))
}
#[cfg(feature = "sqlite")]
fn json<T: serde::Serialize>(keys: &[T]) -> String {
serde_json::to_string(keys).expect("strings and numbers serialize")
}
#[cfg(feature = "sqlite")]
async fn apply<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
message: &Down,
by: &By,
care: &Care,
) -> Result<Vec<Applied>, Stop> {
let naming = !message.snapshot_id.is_empty();
let mut parts = Vec::new();
let foreign = foreign_rows(codecs, &message.tables);
for (entry, table) in message.tables.iter().enumerate() {
if let Some(codec) = codec_of(codecs, &table.table)
.filter(|c| naming && c.table.mirror == crate::row::Mirror::Setting)
{
gone_here(conn, codec, &keys_of(codec, &table.recorded)).await?;
}
let Some(codec) = codec_of(codecs, &table.table).filter(|c| c.sent_down()) else {
continue;
};
let deletes = keys_of(codec, &table.deletes);
let beside = parsed(codec, &table.keys);
for part in parts
.iter_mut()
.filter(|p: &&mut Part<P>| p.codec.table.name == codec.table.name)
{
let refused = std::mem::take(&mut part.refused);
let (gone, kept): (Vec<_>, Vec<_>) = refused
.into_iter()
.filter(|(at, _)| {
part.beside[*at]
.as_ref()
.is_none_or(|k| !beside.contains(&Some(k.clone())))
})
.partition(|(at, _)| {
part.beside[*at]
.as_ref()
.is_some_and(|k| deletes.contains(k))
});
part.refused = kept;
if naming {
let keys: Vec<Key> = gone
.iter()
.filter_map(|(at, _)| part.beside[*at].clone())
.collect();
let held = codec.present(conn, &keys).await?;
named(
conn,
codec.table.name,
&held.iter().map(key_text).collect::<Vec<_>>(),
)
.await?;
}
}
let_go(conn, codecs, codec, &deletes, by, care).await?;
let mut last = HashMap::new();
for (at, key) in beside.iter().enumerate() {
if let Some(key) = key {
last.insert(key, at);
}
}
let deleted = deleted(conn, codec, &beside).await?;
let held_back: Vec<String> = deleted.iter().map(key_text).collect();
if !held_back.is_empty() {
reject(conn, codec.table.name, &held_back, HELD_BACK, true, by).await?;
}
let mut pending = Vec::new();
for at in (0..beside.len()).filter(|at| {
beside[*at]
.as_ref()
.is_none_or(|k| last[k] == *at && !deleted.contains(k))
}) {
match care.culprit(&table.table, &Spot::Upsert { entry, at }) {
Some(why) => {
let key = beside[at].as_ref();
refuse(conn, codec, key, why, naming, by).await?;
}
None => pending.push(at),
}
}
let mut part = Part {
entry,
codec,
table,
beside,
held: Vec::new(),
refused: Vec::new(),
};
part.attempt(conn, &pending, naming, care).await?;
while part.again(conn, naming, care).await? {}
parts.push(part);
}
loop {
let mut took = false;
for part in &mut parts {
took |= part.again(conn, naming, care).await?;
}
if !took {
break;
}
}
for (table, keys) in &foreign {
let why = if codec_of(codecs, table).is_some() {
"a table the server does not send"
} else {
"a table this replica does not mirror"
};
reject(conn, table, keys, why, false, by).await?;
}
let mut taken = Vec::new();
let mut culprits = Vec::new();
for part in parts {
for (at, why) in &part.refused {
let key = part.beside[*at].as_ref();
refuse(conn, part.codec, key, why, naming, by).await?;
}
let keys: Vec<Key> = part.held.iter().map(|(_, key)| key.clone()).collect();
let orphans: HashSet<Key> = part
.codec
.unparented(conn, &keys)
.await?
.into_iter()
.collect();
culprits.extend(
part.held
.iter()
.filter(|(_, key)| orphans.contains(key))
.map(|(at, _)| Culprit {
table: part.codec.table.name.to_owned(),
row: Spot::Upsert {
entry: part.entry,
at: *at,
},
why: "a row it points at, through a foreign key, is not held here".to_owned(),
}),
);
taken.push(Applied {
table: part.table.table.clone(),
keys,
});
}
if !culprits.is_empty() {
return Err(Stop::Culprits(culprits));
}
Ok(taken)
}
#[cfg(feature = "sqlite")]
const HELD_BACK: &str = "deleted here, and the delete not filed yet";
#[cfg(feature = "sqlite")]
async fn deleted<P: Sync + 'static>(
conn: &mut Conn,
codec: &Codec<P>,
keys: &[Option<Key>],
) -> Result<HashSet<Key>, Error> {
if !codec.takes_deletes() {
return Ok(HashSet::new());
}
let keys: Vec<Key> = keys.iter().flatten().cloned().collect();
let noted: HashSet<String> = sqlx::query_scalar(
"SELECT row_key FROM sync_notes WHERE relation = $1 \
AND row_key IN (SELECT value FROM json_each($2))",
)
.bind(codec.table.name)
.bind(json(&keys.iter().map(key_text).collect::<Vec<_>>()))
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
let noted: Vec<Key> = keys
.into_iter()
.filter(|key| noted.contains(&key_text(key)))
.collect();
let held: HashSet<Key> = codec.present(conn, ¬ed).await?.into_iter().collect();
Ok(noted
.into_iter()
.filter(|key| !held.contains(key))
.collect())
}
#[cfg(feature = "sqlite")]
struct Part<'m, P: 'static> {
entry: usize,
codec: &'m Codec<P>,
table: &'m Table,
beside: Vec<Option<Key>>,
held: Vec<(usize, Key)>,
refused: Vec<(usize, String)>,
}
#[cfg(feature = "sqlite")]
impl<P: Sync + 'static> Part<'_, P> {
async fn again(&mut self, conn: &mut Conn, naming: bool, care: &Care) -> Result<bool, Stop> {
let refused: Vec<usize> = std::mem::take(&mut self.refused)
.into_iter()
.map(|(at, _)| at)
.collect();
let held = self.held.len();
self.attempt(conn, &refused, naming, care).await?;
Ok(self.held.len() > held)
}
async fn attempt(
&mut self,
conn: &mut Conn,
positions: &[usize],
naming: bool,
care: &Care,
) -> Result<(), Stop> {
if positions.is_empty() {
return Ok(());
}
if !care.careful() {
let (held, refused) = take(
conn,
self.codec,
self.table,
&self.beside,
positions,
naming,
)
.await?;
self.held.extend(held);
self.refused.extend(refused);
return Ok(());
}
for at in positions {
row_begun(conn).await?;
let one = std::slice::from_ref(at);
let took = take(conn, self.codec, self.table, &self.beside, one, naming).await;
let row = Spot::Upsert {
entry: self.entry,
at: *at,
};
match row_done(conn, took, &self.table.table, row).await? {
Ok((held, refused)) => {
self.held.extend(held);
self.refused.extend(refused);
}
Err(e) => self.refused.push((*at, e.to_string())),
}
}
Ok(())
}
}
const TEXT_CAP: usize = 512;
#[cfg(feature = "sqlite")]
fn foreign_rows<'a, P: Sync + 'static>(
codecs: &[Codec<P>],
tables: &'a [Table],
) -> Vec<(&'a str, Vec<String>)> {
let mut foreign: Vec<(&str, Vec<String>)> = Vec::new();
for table in tables {
if codec_of(codecs, &table.table).is_some_and(|c| c.sent_down()) {
continue;
}
let keys = foreign_keys(table);
match foreign.iter_mut().find(|(name, _)| *name == table.table) {
Some((_, all)) => all.extend(keys),
None => foreign.push((&table.table, keys)),
}
}
foreign
}
#[cfg(feature = "sqlite")]
fn foreign_keys(table: &Table) -> Vec<String> {
let keys: Vec<String> = table.keys.iter().map(|k| k.parts.join("|")).collect();
if table.table.len() > TEXT_CAP || keys.iter().any(|k| k.len() > TEXT_CAP) {
tracing::error!(
"sync: rows of a table named or keyed longer than {TEXT_CAP} bytes — not kept"
);
return Vec::new();
}
keys
}
fn capped(text: &str) -> &str {
let mut end = text.len().min(TEXT_CAP);
while !text.is_char_boundary(end) {
end -= 1;
}
&text[..end]
}
#[cfg(feature = "sqlite")]
async fn take<P: Sync + 'static>(
conn: &mut Conn,
codec: &Codec<P>,
table: &Table,
beside: &[Option<Key>],
positions: &[usize],
naming: bool,
) -> Result<(Vec<(usize, Key)>, Vec<(usize, String)>), Stop> {
let rows = positions
.iter()
.map(|at| (table.upserts[*at].clone(), beside[*at].clone()))
.collect();
let refused: Vec<(usize, String)> = codec
.apply_down(conn, rows)
.await?
.into_iter()
.map(|refused| (positions[refused.at], refused.reason))
.collect();
let held: Vec<(usize, Key)> = positions
.iter()
.filter(|at| !refused.iter().any(|(r, _)| r == *at))
.filter_map(|at| Some((*at, beside[*at].clone()?)))
.collect();
let texts: Vec<String> = held.iter().map(|(_, key)| key_text(key)).collect();
cleared(conn, &table.table, &texts).await?;
sqlx::query(
"DELETE FROM sync_leaving WHERE relation = $1 \
AND row_key IN (SELECT value FROM json_each($2))",
)
.bind(&table.table)
.bind(json(&texts))
.execute(&mut *conn)
.await?;
if naming {
named(conn, &table.table, &texts).await?;
}
Ok((held, refused))
}
#[cfg(feature = "sqlite")]
async fn refuse<P: Sync + 'static>(
conn: &mut Conn,
codec: &Codec<P>,
key: Option<&Key>,
why: &str,
naming: bool,
by: &By,
) -> Result<(), Error> {
let table = codec.table.name;
reject(
conn,
table,
&Vec::from_iter(key.map(key_text)),
why,
false,
by,
)
.await?;
if naming {
let held = codec.present(conn, &Vec::from_iter(key.cloned())).await?;
named(conn, table, &held.iter().map(key_text).collect::<Vec<_>>()).await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
async fn named(conn: &mut Conn, table: &str, keys: &[String]) -> Result<(), Error> {
sqlx::query(
"INSERT OR IGNORE INTO sync_snapshot_keys (table_name, row_key) \
SELECT $1, value FROM json_each($2)",
)
.bind(table)
.bind(json(keys))
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
async fn let_go<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
codec: &Codec<P>,
keys: &[Key],
by: &By,
care: &Care,
) -> Result<(), Stop> {
if !care.careful() {
let_go_together(conn, codecs, codec, keys, by).await?;
return Ok(());
}
let table = codec.table.name;
let mut pending = Vec::new();
for key in keys {
match care.culprit(table, &Spot::LetGo(key_text(key))) {
Some(why) => reject(conn, table, &[key_text(key)], why, false, by).await?,
None => pending.push(key.clone()),
}
}
loop {
let mut again = Vec::new();
for key in &pending {
row_begun(conn).await?;
let one = std::slice::from_ref(key);
let gone = let_go_together(conn, codecs, codec, one, by).await;
match row_done(conn, gone, table, Spot::LetGo(key_text(key))).await? {
Ok(refused) => again.extend(refused),
Err(e) => reject(conn, table, &[key_text(key)], &e.to_string(), false, by).await?,
}
}
if again.is_empty() || again.len() == pending.len() {
return Ok(());
}
pending = again;
}
}
#[cfg(feature = "sqlite")]
async fn let_go_together<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
codec: &Codec<P>,
keys: &[Key],
by: &By,
) -> Result<Vec<Key>, Stop> {
let noted: HashSet<String> = if matches!(codec.table.mirror, crate::row::Mirror::Both) {
sqlx::query_scalar(
"SELECT row_key FROM sync_notes WHERE relation = $1 \
AND row_key IN (SELECT value FROM json_each($2))",
)
.bind(codec.table.name)
.bind(json(&keys.iter().map(key_text).collect::<Vec<_>>()))
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect()
} else {
HashSet::new()
};
let (held, free): (Vec<Key>, Vec<Key>) = keys
.iter()
.cloned()
.partition(|key| noted.contains(&key_text(key)));
let server: Vec<(&str, Option<&str>)> = codecs
.iter()
.filter(|c| c.sent_down())
.map(|c| (c.table.name, c.table.retired))
.collect();
let retired = codec.retire(conn, &free, &held, &server).await?;
let refused: HashSet<&Key> = retired.refused.iter().map(|(key, _)| key).collect();
let leaving: Vec<String> = held
.iter()
.filter(|key| !refused.contains(key))
.map(key_text)
.collect();
sqlx::query(
"INSERT INTO sync_leaving (relation, row_key, epoch, sequence) \
SELECT $1, value, $3, $4 FROM json_each($2) WHERE true \
ON CONFLICT (relation, row_key) DO UPDATE SET epoch = EXCLUDED.epoch, \
sequence = EXCLUDED.sequence",
)
.bind(codec.table.name)
.bind(json(&leaving))
.bind(&by.epoch)
.bind(by.sequence)
.execute(&mut *conn)
.await?;
for (key, why) in &retired.refused {
reject(conn, codec.table.name, &[key_text(key)], why, false, by).await?;
}
let gone: Vec<String> = keys
.iter()
.filter(|key| !refused.contains(key))
.map(key_text)
.collect();
cleared(conn, codec.table.name, &gone).await?;
tracing::debug!(
table = codec.table.name,
deleted = retired.deleted,
kept = retired.kept,
refused = retired.refused.len(),
"sync: rows let go of — the kept ones retired, still pointed at here or with edits on their way"
);
Ok(retired.refused.into_iter().map(|(key, _)| key).collect())
}
#[cfg(feature = "sqlite")]
async fn reject(
conn: &mut Conn,
table: &str,
keys: &[String],
why: &str,
held_back: bool,
by: &By,
) -> Result<(), Error> {
if !held_back {
tracing::error!(
table = capped(table),
rows = keys.len(),
first = capped(keys.first().map_or("", String::as_str)),
sequence = by.sequence,
"sync: rows this replica cannot take or let go of — rejected, for the server to hear of: {}",
capped(why)
);
}
sqlx::query(
"INSERT INTO sync_rejections (table_name, row_key, reason, epoch, sequence, held_back) \
SELECT $1, value, $3, $5, $4, $6 FROM json_each($2) WHERE true \
ON CONFLICT (table_name, row_key) DO UPDATE SET reason = EXCLUDED.reason, \
epoch = EXCLUDED.epoch, sequence = EXCLUDED.sequence, held_back = EXCLUDED.held_back, \
rejected_at = EXCLUDED.rejected_at, reported_at = NULL",
)
.bind(table)
.bind(json(keys))
.bind(capped(why))
.bind(by.sequence)
.bind(&by.epoch)
.bind(held_back)
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
async fn cleared(conn: &mut Conn, table: &str, keys: &[String]) -> Result<(), Error> {
sqlx::query(
"DELETE FROM sync_rejections WHERE table_name = $1 \
AND row_key IN (SELECT value FROM json_each($2))",
)
.bind(table)
.bind(json(keys))
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn collect<P: Sync + 'static>(
pool: &sqlx::SqlitePool,
codecs: &[Codec<P>],
) -> Result<(), Error> {
let server: Vec<&Codec<P>> = codecs.iter().filter(|c| c.sent_down()).rev().collect();
let mut conn = pool.acquire().await?;
let refusal = match collected(&mut conn, &server, None).await {
Ok(_) => return Ok(()),
Err(e) if e.is_refusal() => e,
Err(e) => return Err(e),
};
conn.close_on_drop();
drop(conn);
tracing::warn!("sync: retired rows refused to go together, let go of one by one: {refusal}");
loop {
let mut went = 0;
for codec in &server {
let keys = collectable(&mut *pool.acquire().await?, codec).await?;
for key in &keys {
let mut conn = pool.acquire().await?;
match collected(&mut conn, &[codec], Some(key)).await {
Ok(gone) => went += gone,
Err(e) if e.is_refusal() => conn.close_on_drop(),
Err(e) => return Err(e),
}
}
}
if went == 0 {
return Ok(());
}
}
}
#[cfg(feature = "sqlite")]
async fn collected<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[&Codec<P>],
one: Option<&Key>,
) -> Result<u64, Error> {
let mut tx = sqlx::Connection::begin_with(&mut *conn, "BEGIN IMMEDIATE").await?;
sqlx::query("INSERT INTO sync_applying (id) VALUES (1)")
.execute(&mut *tx)
.await?;
let mut gone = 0;
for codec in codecs {
let mut keys = collectable(&mut tx, codec).await?;
if let Some(one) = one {
keys.retain(|key| key == one);
}
gone += codec.collect(&mut tx, &keys).await?;
}
sqlx::query("DELETE FROM sync_applying")
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(gone)
}
#[cfg(feature = "sqlite")]
async fn collectable<P: Sync + 'static>(
conn: &mut Conn,
codec: &Codec<P>,
) -> Result<Vec<Key>, Error> {
let noted: HashSet<String> =
sqlx::query_scalar("SELECT row_key FROM sync_notes WHERE relation = $1")
.bind(codec.table.name)
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
Ok(codec
.retired_keys(conn)
.await?
.into_iter()
.filter(|key| !noted.contains(&key_text(key)))
.collect())
}
#[cfg(feature = "sqlite")]
async fn end_snapshot<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
message: &EndSnapshot,
by: &By,
care: &Care,
) -> Result<(), Stop> {
let began: i64 = sqlx::query_scalar("SELECT snapshot_began FROM sync_applied WHERE id = 1")
.fetch_one(&mut *conn)
.await?;
let mut seen = HashSet::new();
let tables: Vec<&String> = message.tables.iter().filter(|t| seen.insert(*t)).collect();
for table in tables.into_iter().rev() {
sqlx::query(
"DELETE FROM sync_rejections WHERE table_name = $1 \
AND (sequence < $2 OR epoch <> (SELECT epoch FROM sync_applied WHERE id = 1))",
)
.bind(table)
.bind(began)
.execute(&mut *conn)
.await?;
let Some(codec) = codec_of(codecs, table).filter(|c| c.sent_down()) else {
tracing::error!(
table = capped(table),
"sync: a snapshot of a table this replica does not hold from the server — not swept"
);
continue;
};
let named: HashSet<String> =
sqlx::query_scalar("SELECT row_key FROM sync_snapshot_keys WHERE table_name = $1")
.bind(table)
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
let stale: Vec<Key> = codec
.keys(conn)
.await?
.into_iter()
.filter(|key| !named.contains(&key_text(key)))
.collect();
let_go(conn, codecs, codec, &stale, by, care).await?;
}
let held: Vec<&str> = codecs
.iter()
.filter(|c| c.sent_down())
.map(|c| c.table.name)
.collect();
sqlx::query(
"DELETE FROM sync_rejections WHERE table_name NOT IN (SELECT value FROM json_each($1)) \
AND epoch <> (SELECT epoch FROM sync_applied WHERE id = 1)",
)
.bind(json(&held))
.execute(&mut *conn)
.await?;
sqlx::query("DELETE FROM sync_snapshot_keys")
.execute(&mut *conn)
.await?;
sqlx::query("UPDATE sync_applied SET snapshot_id = NULL, snapshot_began = NULL WHERE id = 1")
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
async fn filed<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
message: &envelope::Filed,
care: &Care,
) -> Result<(), Stop> {
let uploads: Vec<i64> = message
.uploads
.iter()
.filter_map(|u| i64::try_from(*u).ok())
.collect();
for sql in [
"DELETE FROM sync_notes WHERE upload IN (SELECT value FROM json_each($1)) \
AND writes = sent",
"UPDATE sync_notes SET restated = 0 WHERE upload IN (SELECT value FROM json_each($1))",
] {
sqlx::query(sql)
.bind(json(&uploads))
.execute(&mut *conn)
.await?;
}
let both: Vec<&str> = codecs
.iter()
.filter(|c| matches!(c.table.mirror, crate::row::Mirror::Both))
.map(|c| c.table.name)
.collect();
let done: Vec<(String, String, String, i64)> = sqlx::query_as(
"DELETE FROM sync_leaving AS l WHERE relation IN (SELECT value FROM json_each($1)) \
AND NOT EXISTS (SELECT 1 FROM sync_notes n \
WHERE n.relation = l.relation AND n.row_key = l.row_key) \
RETURNING relation, row_key, epoch, sequence",
)
.bind(json(&both))
.fetch_all(&mut *conn)
.await?;
let mut by: std::collections::BTreeMap<(&str, By), Vec<Key>> = Default::default();
for (table, key, epoch, sequence) in &done {
let Some(codec) = codec_of(codecs, table) else {
continue;
};
let parts: Vec<String> = key.split('|').map(str::to_owned).collect();
if let Ok(key) = codec.key(&parts) {
let message = By {
epoch: epoch.clone(),
sequence: *sequence,
};
by.entry((codec.table.name, message)).or_default().push(key);
}
}
for ((table, message), keys) in by {
let codec = codec_of(codecs, table).expect("a table of this build's");
let_go(conn, codecs, codec, &keys, &message, care).await?;
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(test, derive(ts_rs::TS))]
#[non_exhaustive]
pub struct UpProgress {
pub table: String,
pub unsent: i64,
pub unfiled: i64,
#[serde(default)]
pub set_aside: i64,
}
#[cfg(feature = "sqlite")]
#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct Told {
table: String,
unsent: u64,
unfiled: u64,
set_aside: u64,
}
#[cfg(feature = "sqlite")]
pub async fn progress(conn: &mut Conn) -> Result<Vec<UpProgress>, Error> {
let rows: Vec<(String, i64, i64, i64)> = sqlx::query_as(
"SELECT relation, \
sum(set_aside IS NULL AND (sent_at IS NULL OR writes > sent)), \
sum(set_aside IS NULL AND sent_at IS NOT NULL AND writes = sent), \
sum(set_aside IS NOT NULL) \
FROM sync_notes GROUP BY relation ORDER BY relation",
)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|(table, unsent, unfiled, set_aside)| UpProgress {
table,
unsent,
unfiled,
set_aside,
})
.collect())
}
#[cfg(feature = "sqlite")]
async fn backlog_due(
conn: &mut Conn,
now: Vec<UpProgress>,
settings: &Outbox,
sending: bool,
) -> Result<Option<envelope::Backlog>, Error> {
let stored: Option<(String, i64)> =
sqlx::query_as("SELECT backlog, reported_at FROM sync_backlog WHERE id = 1")
.fetch_optional(&mut *conn)
.await?;
let (before, at) = match stored {
Some((backlog, at)) => match serde_json::from_str::<Vec<Told>>(&backlog) {
Ok(before) => (Some(before), at),
Err(_) => (None, 0),
},
None => (None, 0),
};
let now = backlog_of(now);
let every = i64::try_from(settings.backlog_every.as_millis()).unwrap_or(i64::MAX);
let clock = chrono::Utc::now().timestamp_millis();
let stale = clock < at || clock - at >= every;
let unchanged = before.as_ref() == Some(&now.tables.iter().map(told).collect());
let due = if unchanged {
!now.tables.is_empty() && stale
} else {
let edge = now.tables.is_empty() || before.as_ref().is_none_or(Vec::is_empty);
sending || edge || stale
};
Ok(due.then_some(now))
}
#[cfg(feature = "sqlite")]
fn told(table: &envelope::TableBacklog) -> Told {
Told {
table: table.table.clone(),
unsent: table.unsent,
unfiled: table.unfiled,
set_aside: table.set_aside,
}
}
#[cfg(feature = "sqlite")]
fn backlog_of(progress: Vec<UpProgress>) -> envelope::Backlog {
let count = |n: i64| u64::try_from(n).unwrap_or_default();
envelope::Backlog {
tables: progress
.into_iter()
.map(|t| envelope::TableBacklog {
table: t.table,
unsent: count(t.unsent),
unfiled: count(t.unfiled),
set_aside: count(t.set_aside),
})
.collect(),
}
}
#[cfg(feature = "sqlite")]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Lane {
pub name: &'static str,
pub tables: &'static [&'static str],
}
#[cfg(feature = "sqlite")]
impl Lane {
#[must_use]
pub const fn new(name: &'static str, tables: &'static [&'static str]) -> Self {
Lane { name, tables }
}
}
#[cfg(feature = "sqlite")]
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Outbox {
pub limits: envelope::Limits,
pub resend_after: std::time::Duration,
pub backlog_every: std::time::Duration,
}
#[cfg(feature = "sqlite")]
impl Default for Outbox {
fn default() -> Self {
Outbox {
limits: envelope::Limits::default(),
resend_after: std::time::Duration::from_secs(600),
backlog_every: std::time::Duration::from_secs(60),
}
}
}
#[cfg(feature = "sqlite")]
pub(crate) fn check_settings<P: Sync + 'static>(
codecs: &[Codec<P>],
lanes: &[Lane],
) -> Result<(), Error> {
let misuse = |why: String| Err(Error::Misuse(why));
if lanes.is_empty() {
return misuse("a replica needs a lane to send up on".into());
}
if lanes.iter().filter(|l| l.tables.is_empty()).count() > 1 {
return misuse("one lane at most carries every table no other lane names".into());
}
let mut names = HashSet::new();
if let Some(lane) = lanes.iter().find(|l| !names.insert(l.name)) {
return misuse(format!("two lanes are named `{}`", lane.name));
}
let mut named = HashSet::new();
for table in lanes.iter().flat_map(|l| l.tables.iter()) {
if !named.insert(*table) {
return misuse(format!("`{table}` is on two lanes"));
}
if !codec_of(codecs, table).is_some_and(Codec::goes_up) {
return misuse(format!("a lane carries `{table}`, which does not go up"));
}
}
if lanes.iter().all(|l| !l.tables.is_empty()) {
if let Some(codec) = codecs.iter().find(|c| {
c.goes_up() && !named.contains(c.table.name) && c.table.name != crate::wish::TABLE
}) {
return misuse(format!("`{}` goes up on no lane", codec.table.name));
}
}
Ok(())
}
#[cfg(feature = "sqlite")]
#[derive(sqlx::FromRow)]
struct Applying {
epoch: String,
sequence: i64,
nonce: i64,
mac: Vec<u8>,
acknowledged: i64,
gap: i64,
gap_told: i64,
}
#[cfg(feature = "sqlite")]
#[derive(Debug)]
pub(crate) struct Outgoing {
pub(crate) lane: &'static str,
pub(crate) up: Up,
notes: Vec<i64>,
rejected: Vec<(String, String, i64, bool)>,
}
#[cfg(feature = "sqlite")]
impl Outgoing {
pub(crate) fn bytes(&self) -> Vec<u8> {
self.up.encode_to_vec()
}
pub(crate) fn items(&self) -> usize {
self.notes.len() + self.rejected.len()
}
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Clone, Copy)]
pub(crate) struct Window {
pub(crate) take: usize,
pub(crate) skip: usize,
}
#[cfg(feature = "sqlite")]
pub(crate) async fn outbox<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
lanes: &[Lane],
lane: usize,
settings: &Outbox,
digests: Option<Held>,
window: Option<Window>,
) -> Result<Option<Outgoing>, Error> {
let Some(this) = lanes.get(lane) else {
return Err(Error::Misuse(format!("no lane {lane} of {}", lanes.len())));
};
let main = lane == 0;
let mut up = Up {
protocol: envelope::PROTOCOL,
..Up::default()
};
let limit = settings.limits.bytes;
let mut reserved = framed(9);
let mut rejected = Vec::new();
let mut untold_gap = false;
let mut news = false;
let mut items = settings
.limits
.rows
.min(window.map_or(usize::MAX, |w| w.take));
let mut skip = window.map_or(0, |w| w.skip);
let mut backlog = None;
if main {
let applied: Option<Applying> = sqlx::query_as(
"SELECT epoch, sequence, nonce, mac, acknowledged, gap, gap_told FROM sync_applied \
WHERE id = 1",
)
.fetch_optional(&mut *conn)
.await?;
reserved += applied
.as_ref()
.map_or(0, |a| framed(a.epoch.len()) + framed(a.mac.len()) + 3 * 11);
let tables: usize = progress(conn)
.await?
.iter()
.map(|t| framed(framed(t.table.len()) + 3 * 11))
.sum();
if up.encoded_len() + reserved + framed(tables) <= limit {
reserved += framed(tables);
backlog = Some(framed(tables));
} else {
tracing::error!(
bytes = framed(tables),
limit,
"sync: a backlog larger than a message carries — not told"
);
}
let mut pending = unreported(conn, skip.saturating_add(items).saturating_add(1)).await?;
let passed = skip.min(pending.len());
pending.drain(..passed);
skip -= passed;
let mut all_reported = passed == 0 && pending.len() <= items;
for (row, sequence) in pending.into_iter().take(items) {
let weight = framed(row.encoded_len());
if up.encoded_len() + reserved + weight > limit && up.rejected.is_empty() {
reserved -= backlog.take().unwrap_or_default();
}
if up.encoded_len() + reserved + weight > limit {
all_reported = false;
break;
}
let key = row
.key
.as_ref()
.map(|k| k.parts.join("|"))
.unwrap_or_default();
let mut kept = false;
if let Some(codec) = codec_of(codecs, &row.table).filter(|c| c.sent_down()) {
let parts = row
.key
.as_ref()
.map(|k| k.parts.clone())
.unwrap_or_default();
if let Ok(key) = codec.key(&parts) {
kept = !codec.present(conn, &[key]).await?.is_empty();
}
}
rejected.push((row.table.clone(), key, sequence, kept));
up.rejected.push(row);
}
items -= up.rejected.len();
if let Some(a) = applied {
up.gap = u64::try_from(a.gap).unwrap_or_default();
untold_gap = a.gap > a.gap_told;
if all_reported {
let read = Some((a.epoch.clone(), a.sequence));
if let Some(mut held) = digests.filter(|held| held.applied == read) {
let noted: HashSet<String> =
sqlx::query_scalar("SELECT DISTINCT relation FROM sync_notes")
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
held.digests.retain(|d| !noted.contains(&d.table));
let weight: usize = held.digests.iter().map(|d| framed(d.encoded_len())).sum();
if up.encoded_len() + reserved + weight <= limit {
up.digests = held.digests;
}
}
news = a.sequence > a.acknowledged;
if news || !up.digests.is_empty() || a.gap > 0 {
up.epoch = a.epoch;
up.sequence = u64::try_from(a.sequence).unwrap_or_default();
up.nonce = u64::try_from(a.nonce).unwrap_or_default();
up.mac = a.mac;
}
}
}
}
let bare = Up {
protocol: envelope::PROTOCOL,
..Up::default()
};
let largest = limit.saturating_sub(bare.encoded_len() + reserved);
let budget = limit.saturating_sub(up.encoded_len() + reserved);
let notes = unsent(conn, lanes, this, settings, items, skip).await?;
let gathered = gather(conn, codecs, notes, largest, budget).await?;
up.tables = gathered.tables;
if !gathered.carried.is_empty() {
let number: i64 = sqlx::query_scalar(
"SELECT CASE WHEN n = 0 THEN 1 ELSE n END \
FROM (SELECT random() & 9223372036854775807 AS n)",
)
.fetch_one(&mut *conn)
.await?;
sqlx::query(
"UPDATE sync_notes SET upload = $1, sent = writes, sent_at = NULL \
WHERE id IN (SELECT value FROM json_each($2))",
)
.bind(number)
.bind(json(&gathered.carried))
.execute(&mut *conn)
.await?;
up.uploads = vec![u64::try_from(number).unwrap_or_default()];
}
let sending = !up.tables.is_empty()
|| !up.rejected.is_empty()
|| !up.digests.is_empty()
|| news
|| untold_gap;
if backlog.is_some() {
let now = progress(conn).await?;
up.backlog = backlog_due(conn, now, settings, sending).await?;
}
Ok((sending || up.backlog.is_some()).then_some(Outgoing {
lane: this.name,
up,
notes: gathered.carried,
rejected,
}))
}
#[cfg(feature = "sqlite")]
#[derive(sqlx::FromRow)]
struct Note {
id: i64,
relation: String,
row_key: String,
restated: bool,
}
#[cfg(feature = "sqlite")]
async fn unsent(
conn: &mut Conn,
lanes: &[Lane],
lane: &Lane,
settings: &Outbox,
most: usize,
skip: usize,
) -> Result<Vec<Note>, Error> {
let quoted = |tables: &mut dyn Iterator<Item = &&str>| {
tables.map(|t| literal(t)).collect::<Vec<_>>().join(", ")
};
let wishes = (std::ptr::eq(lane, &lanes[0])
&& lanes.iter().all(|l| !l.tables.is_empty())
&& !lanes.iter().any(|l| l.tables.contains(&crate::wish::TABLE)))
.then_some(&crate::wish::TABLE);
let filter = if lane.tables.is_empty() {
let others = quoted(&mut lanes.iter().flat_map(|l| l.tables.iter()));
if others.is_empty() {
String::new()
} else {
format!("AND relation NOT IN ({others})")
}
} else {
format!(
"AND relation IN ({})",
quoted(&mut lane.tables.iter().chain(wishes))
)
};
let now = crate::dialect::NOW;
Ok(sqlx::query_as(&format!(
"SELECT id, relation, row_key, restated FROM sync_notes \
WHERE set_aside IS NULL \
AND (sent_at IS NULL OR writes > sent \
OR sent_at < strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-{:.3} seconds') \
OR sent_at > {now}) {filter} \
ORDER BY id LIMIT $1 OFFSET $2",
settings.resend_after.as_secs_f64()
))
.bind(i64::try_from(settings.limits.rows.min(most)).unwrap_or(i64::MAX))
.bind(i64::try_from(skip).unwrap_or(i64::MAX))
.fetch_all(&mut *conn)
.await?)
}
#[cfg(feature = "sqlite")]
type Keyed = Vec<(Key, i64)>;
#[cfg(feature = "sqlite")]
struct Gathered {
tables: Vec<Table>,
carried: Vec<i64>,
}
#[cfg(feature = "sqlite")]
async fn gather<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
notes: Vec<Note>,
largest: usize,
mut budget: usize,
) -> Result<Gathered, Error> {
let mut aside: Vec<(i64, String)> = Vec::new();
let mut done: Vec<i64> = Vec::new();
let mut keyed: HashMap<(&str, bool), Keyed> = HashMap::new();
for note in ¬es {
let Some(codec) = codec_of(codecs, ¬e.relation).filter(|c| c.goes_up()) else {
aside.push((note.id, "a table this build does not send up".to_owned()));
continue;
};
let parts: Vec<String> = note.row_key.split('|').map(str::to_owned).collect();
let key = match codec.key(&parts) {
Ok(key) => key,
Err(e) => {
aside.push((note.id, e.to_string()));
continue;
}
};
keyed
.entry((codec.table.name, note.restated))
.or_default()
.push((key, note.id));
}
let mut carried = Vec::new();
let mut tables = Vec::new();
let mut gone: Vec<(&Codec<P>, Keyed)> = Vec::new();
let mut full = false;
for (codec, restated) in codecs
.iter()
.flat_map(|codec| [(codec, true), (codec, false)])
{
let Some(keys) = keyed.remove(&(codec.table.name, restated)) else {
continue;
};
let wanted: Vec<Key> = keys.iter().map(|(k, _)| k.clone()).collect();
let encoded = codec.load(conn, &wanted).await?;
let mut part = Table {
table: codec.table.name.to_owned(),
restated,
..Table::default()
};
let mut len = framed(part.table.len()) + usize::from(restated) * 2;
let mut missing = Vec::new();
for (key, id) in keys {
if let Some((_, e)) = encoded.failed.iter().find(|(k, _)| *k == key) {
aside.push((id, e.to_string()));
continue;
}
let Some((_, bytes)) = encoded.rows.iter().find(|(k, _)| *k == key) else {
missing.push((key, id));
continue;
};
let wire = wire_of(&key);
let added = framed(bytes.len()) + framed(wire.encoded_len());
if framed(framed(part.table.len()) + usize::from(restated) * 2 + added) > largest {
let why = format!(
"a row of {} bytes, more than a message carries",
bytes.len()
);
aside.push((id, why));
continue;
}
let weight = growth(len, added, part.upserts.is_empty());
if full || weight > budget {
full = true;
continue;
}
budget -= weight;
len += added;
part.upserts.push(bytes.clone());
part.keys.push(wire);
carried.push(id);
}
if !part.upserts.is_empty() {
tables.push(part);
}
if codec.takes_deletes() {
match gone.last_mut() {
Some((last, keys)) if std::ptr::eq(*last, codec) => keys.extend(missing),
_ => gone.push((codec, missing)),
}
} else {
done.extend(missing.into_iter().map(|(_, id)| id));
}
}
for (codec, keys) in gone.into_iter().rev() {
let mut part = Table {
table: codec.table.name.to_owned(),
..Table::default()
};
let mut len = framed(part.table.len());
for (key, id) in keys {
let wire = wire_of(&key);
let added = framed(wire.encoded_len());
let weight = growth(len, added, part.deletes.is_empty());
if full || weight > budget {
full = true;
continue;
}
budget -= weight;
len += added;
part.deletes.push(wire);
carried.push(id);
}
if !part.deletes.is_empty() {
tables.push(part);
}
}
for (id, why) in &aside {
tracing::error!(
note = id,
"sync: a write that cannot go up as it is — set aside: {why}"
);
sqlx::query("UPDATE sync_notes SET set_aside = $2 WHERE id = $1")
.bind(id)
.bind(why)
.execute(&mut *conn)
.await?;
}
sqlx::query("DELETE FROM sync_notes WHERE id IN (SELECT value FROM json_each($1))")
.bind(json(&done))
.execute(&mut *conn)
.await?;
Ok(Gathered { tables, carried })
}
#[cfg(feature = "sqlite")]
fn growth(len: usize, added: usize, new: bool) -> usize {
framed(len + added) - if new { 0 } else { framed(len) }
}
#[cfg(feature = "sqlite")]
fn wire_of(key: &Key) -> envelope::Key {
envelope::Key {
parts: key.iter().map(ToString::to_string).collect(),
}
}
#[cfg(feature = "sqlite")]
async fn unreported(
conn: &mut Conn,
limit: usize,
) -> Result<Vec<(envelope::Rejected, i64)>, Error> {
let rows: Vec<(String, String, String, String, i64, bool)> = sqlx::query_as(
"SELECT table_name, row_key, reason, epoch, sequence, held_back FROM sync_rejections \
WHERE reported_at IS NULL AND epoch = (SELECT epoch FROM sync_applied WHERE id = 1) \
ORDER BY rejected_at, table_name, row_key LIMIT $1",
)
.bind(i64::try_from(limit).unwrap_or(i64::MAX))
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.map(|(table, key, reason, epoch, sequence, held_back)| {
let row = envelope::Rejected {
table,
key: Some(envelope::Key {
parts: key.split('|').map(str::to_owned).collect(),
}),
reason,
epoch,
sequence: u64::try_from(sequence).unwrap_or_default(),
held_back,
};
(row, sequence)
})
.collect())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn delivered(conn: &mut Conn, outgoing: &Outgoing) -> Result<(), Error> {
if let Some(number) = outgoing.up.uploads.first() {
sqlx::query(&format!(
"UPDATE sync_notes SET sent_at = {} \
WHERE upload = $1 AND id IN (SELECT value FROM json_each($2))",
crate::dialect::NOW
))
.bind(i64::try_from(*number).unwrap_or(i64::MAX))
.bind(json(&outgoing.notes))
.execute(&mut *conn)
.await?;
}
for (table, key, sequence, kept) in &outgoing.rejected {
let told = if *kept {
format!(
"UPDATE sync_rejections SET reported_at = {}",
crate::dialect::NOW
)
} else {
"DELETE FROM sync_rejections".to_owned()
};
sqlx::query(&format!(
"{told} WHERE table_name = $1 AND row_key = $2 AND sequence = $3"
))
.bind(table)
.bind(key)
.bind(sequence)
.execute(&mut *conn)
.await?;
}
if let Some(backlog) = &outgoing.up.backlog {
let tables: Vec<Told> = backlog.tables.iter().map(told).collect();
sqlx::query(
"INSERT INTO sync_backlog (id, backlog, reported_at) VALUES (1, $1, $2) \
ON CONFLICT (id) DO UPDATE SET backlog = EXCLUDED.backlog, reported_at = EXCLUDED.reported_at",
)
.bind(sqlx::types::Json(tables))
.bind(chrono::Utc::now().timestamp_millis())
.execute(&mut *conn)
.await?;
}
if outgoing.up.gap > 0 {
sqlx::query(
"UPDATE sync_applied SET gap_told = max(gap_told, $1) WHERE id = 1 AND gap > 0",
)
.bind(i64::try_from(outgoing.up.gap).unwrap_or(i64::MAX))
.execute(&mut *conn)
.await?;
}
sqlx::query(
"UPDATE sync_applied SET acknowledged = max(acknowledged, $2) WHERE id = 1 AND epoch = $1",
)
.bind(&outgoing.up.epoch)
.bind(i64::try_from(outgoing.up.sequence).unwrap_or(i64::MAX))
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn refused(conn: &mut Conn, outgoing: &Outgoing, why: &str) -> Result<(), Error> {
sqlx::query(
"UPDATE sync_notes SET set_aside = $1 \
WHERE upload = $3 AND writes = sent AND id IN (SELECT value FROM json_each($2))",
)
.bind(capped(why))
.bind(json(&outgoing.notes))
.bind(
outgoing
.up
.uploads
.first()
.map(|n| i64::try_from(*n).unwrap_or_default()),
)
.execute(&mut *conn)
.await?;
for (table, key, sequence, _) in &outgoing.rejected {
tracing::error!(
table,
key,
"sync: a rejection the uplink refuses for good — taken as told: {why}"
);
sqlx::query(&format!(
"UPDATE sync_rejections SET reported_at = {} \
WHERE table_name = $1 AND row_key = $2 AND sequence = $3",
crate::dialect::NOW
))
.bind(table)
.bind(key)
.bind(sequence)
.execute(&mut *conn)
.await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn retry_set_aside(conn: &mut Conn) -> Result<u64, Error> {
Ok(
sqlx::query("UPDATE sync_notes SET set_aside = NULL WHERE set_aside IS NOT NULL")
.execute(&mut *conn)
.await?
.rows_affected(),
)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn digests_due(
conn: &mut Conn,
first: std::time::Duration,
every: std::time::Duration,
) -> Result<bool, Error> {
let millis = |d: std::time::Duration| i64::try_from(d.as_millis()).unwrap_or(i64::MAX);
let now = chrono::Utc::now().timestamp_millis();
sqlx::query("INSERT OR IGNORE INTO sync_digests (id, due) VALUES (1, $1)")
.bind(now.saturating_add(millis(first)))
.execute(&mut *conn)
.await?;
let due: i64 = sqlx::query_scalar("SELECT due FROM sync_digests WHERE id = 1")
.fetch_one(&mut *conn)
.await?;
Ok(now >= due || due > now.saturating_add(millis(first.max(every))))
}
#[cfg(feature = "sqlite")]
pub(crate) async fn digested(conn: &mut Conn, every: std::time::Duration) -> Result<(), Error> {
let every = i64::try_from(every.as_millis()).unwrap_or(i64::MAX);
sqlx::query("INSERT INTO sync_digests (id, due) VALUES (1, $1) ON CONFLICT (id) DO UPDATE SET due = EXCLUDED.due")
.bind(chrono::Utc::now().timestamp_millis().saturating_add(every))
.execute(&mut *conn)
.await?;
Ok(())
}
#[cfg(feature = "sqlite")]
#[derive(Debug)]
pub(crate) struct Held {
applied: Option<(String, i64)>,
digests: Vec<envelope::Digest>,
}
#[cfg(feature = "sqlite")]
pub(crate) async fn held<P: Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
) -> Result<Held, Error> {
let applied = sqlx::query_as("SELECT epoch, sequence FROM sync_applied WHERE id = 1")
.fetch_optional(&mut *conn)
.await?;
let mut digests = Vec::new();
let setting = |c: &Codec<P>| c.table.mirror == crate::row::Mirror::Setting;
for codec in codecs.iter().filter(|c| c.sent_down() || setting(c)) {
let unfiled: bool =
sqlx::query_scalar("SELECT EXISTS (SELECT 1 FROM sync_notes WHERE relation = $1)")
.bind(codec.table.name)
.fetch_one(&mut *conn)
.await?;
if unfiled {
continue;
}
let rejected: HashSet<String> =
sqlx::query_scalar("SELECT row_key FROM sync_rejections WHERE table_name = $1")
.bind(codec.table.name)
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
let mut keys: Vec<String> = codec
.live_keys(conn)
.await?
.iter()
.map(key_text)
.filter(|k| !rejected.contains(k))
.collect();
if setting(codec) {
keys = codec.versions(conn, None).await?;
} else if codec.table.name == crate::wish::TABLE {
keys = crate::wish::answers(conn, keys).await?;
}
digests.push(envelope::Digest {
table: codec.table.name.to_owned(),
count: keys.len() as u64,
fingerprint: envelope::fingerprint(keys),
});
}
Ok(Held { applied, digests })
}
#[cfg(all(test, feature = "sqlite"))]
#[path = "replica_tests.rs"]
mod tests;