use crate::dialect::{ident, literal};
use crate::row::{key_text, Column, Key, KeyPart, Route, Table};
use crate::wire::Codec;
use crate::Error;
use std::collections::{BTreeMap, BTreeSet, HashMap};
use uuid::Uuid;
#[derive(Clone, Copy, Debug)]
pub struct Replicas {
pub(crate) relation: &'static str,
pub(crate) table: &'static str,
pub(crate) live: &'static str,
pub(crate) partition: Option<&'static str>,
}
impl Replicas {
#[must_use]
pub const fn new(relation: &'static str, table: &'static str) -> Self {
Replicas {
relation,
table,
live: "true",
partition: None,
}
}
#[must_use]
pub const fn live(self, condition: &'static str) -> Self {
Replicas {
live: condition,
..self
}
}
#[must_use]
pub const fn partitioned_by(self, column: &'static str) -> Self {
Replicas {
partition: Some(column),
..self
}
}
}
#[derive(Clone, Debug)]
struct Reference {
parent: usize,
columns: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Relation {
schema: String,
name: String,
}
impl Relation {
fn sql(&self) -> String {
format!("{}.{}", ident(&self.schema), ident(&self.name))
}
}
#[derive(Clone, Debug)]
struct Group {
members: Relation,
member_row: Vec<String>,
group: Vec<(String, String)>,
assigned: Relation,
assigned_replica: String,
}
pub(crate) struct Plan {
pub(crate) table: &'static str,
relation: Relation,
active: String,
pub(crate) key: Vec<Column>,
route: Route,
references: Vec<Reference>,
group: Option<Group>,
owned: Vec<&'static str>,
columns: Vec<&'static str>,
size: String,
encoding: &'static str,
}
#[derive(Default, Debug)]
pub(crate) struct Delta {
pub(crate) snapshots: BTreeSet<Uuid>,
pub(crate) notes: Vec<Note>,
pub(crate) blocked: BTreeMap<(Uuid, usize, String), String>,
}
#[derive(Default)]
struct Asked {
changed: BTreeSet<usize>,
regrouped: BTreeSet<usize>,
reassigned: BTreeSet<usize>,
released: bool,
fresh: bool,
}
#[derive(Clone, Debug)]
pub(crate) struct Note {
pub(crate) id: i64,
pub(crate) change: Change,
}
#[derive(Clone, Debug)]
pub(crate) enum Change {
Row {
table: String,
key: String,
retry: bool,
},
Members {
table: String,
member: serde_json::Value,
},
Assigned { table: String, replica: Uuid },
Resync { replica: Uuid },
}
#[derive(Default)]
pub(crate) struct Encoded {
pub(crate) rows: BTreeMap<String, Vec<u8>>,
pub(crate) failed: BTreeMap<String, String>,
}
#[derive(Debug)]
pub(crate) struct Unsent {
pub(crate) replica: Uuid,
pub(crate) table: &'static str,
pub(crate) key: String,
pub(crate) reason: String,
}
type ForeignKey = (Relation, Relation, Vec<String>, Vec<String>);
type CatalogForeignKey = (String, String, String, String, Vec<String>, Vec<String>);
pub(crate) const RECORD_CHUNK: usize = 2_000;
const INSTALL_LOCK: i64 = 0x5359_4e43_494e_5354;
const MIGRATIONS: [&str; crate::SERVER_SCHEMA_VERSION as usize] = [
include_str!("../sql/postgres/1.sql"),
include_str!("../sql/postgres/2.sql"),
];
#[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"
))
}
pub struct Engine {
pub(crate) plans: Vec<Plan>,
by_name: HashMap<&'static str, usize>,
pub(crate) order: Vec<usize>,
pub(crate) replicas: Replicas,
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let tables: Vec<_> = self.order.iter().map(|&i| self.plans[i].table).collect();
f.debug_struct("Engine")
.field("tables", &tables)
.field("replicas", &self.replicas)
.finish_non_exhaustive()
}
}
use crate::Conn;
fn key_sql(alias: &str, columns: &[impl AsRef<str>]) -> String {
let parts: Vec<String> = columns
.iter()
.map(|c| format!("{alias}.{}::text", ident(c.as_ref())))
.collect();
format!("concat_ws('|', {})", parts.join(", "))
}
fn key_in(alias: &str, columns: &[impl AsRef<str>], types: &[Column], param: &str) -> String {
if let ([column], [typed]) = (columns, types) {
return format!(
"{alias}.{} IN (SELECT unnest({param}::text[])::{})",
ident(column.as_ref()),
KeyPart::sql_type(typed.key)
);
}
let row: Vec<String> = columns
.iter()
.map(|c| format!("{alias}.{}", ident(c.as_ref())))
.collect();
let parts: Vec<String> = types
.iter()
.enumerate()
.map(|(i, typed)| {
format!(
"split_part(k, '|', {})::{}",
i + 1,
KeyPart::sql_type(typed.key)
)
})
.collect();
format!(
"({}) IN (SELECT {} FROM unnest({param}::text[]) AS k)",
row.join(", "),
parts.join(", ")
)
}
fn key_eq(alias: &str, columns: &[impl AsRef<str>], types: &[Column], text: &str) -> String {
let row: Vec<String> = columns
.iter()
.map(|c| format!("{alias}.{}", ident(c.as_ref())))
.collect();
let parts: Vec<String> = types
.iter()
.enumerate()
.map(|(i, typed)| {
let part = if types.len() == 1 {
text.to_owned()
} else {
format!("split_part({text}, '|', {})", i + 1)
};
format!("({part})::{}", KeyPart::sql_type(typed.key))
})
.collect();
format!("({}) = ({})", row.join(", "), parts.join(", "))
}
async fn analyze(conn: &mut Conn, tables: &str) -> Result<(), Error> {
sqlx::Executor::execute(&mut *conn, format!("ANALYZE {tables}").as_str()).await?;
Ok(())
}
#[derive(Clone, Copy)]
enum Set {
Suspect,
Blocked,
Kept(usize, bool),
}
pub(crate) const REFUSED: &str = "coalesce(h.refused >= h.sequence, false)";
fn holds(join: &str, alias: &str, replica: &str, relation: &str, key: &str) -> String {
format!(
"{join} LATERAL (SELECT h.replica_id, h.row_key, h.noted, \
{REFUSED} AS refused \
FROM sync.held h \
WHERE h.replica_id = {replica} AND h.relation = {relation} AND h.row_key = {key} \
AND NOT EXISTS (SELECT 1 FROM sync_live provisioned \
WHERE provisioned.replica_id = {replica} AND provisioned.fresh) \
OFFSET 0) {alias} ON true"
)
}
fn holders(relation: &str, key: &str) -> String {
format!(
"CROSS JOIN LATERAL (SELECT h.replica_id FROM sync.held h \
WHERE h.relation = {relation} AND h.row_key = {key} \
AND h.replica_id IN (SELECT replica_id FROM sync_live WHERE NOT fresh) \
OFFSET 0) c"
)
}
fn held_by_key(relation: &str, keys: &str) -> String {
format!(
"SELECT h.replica_id, h.row_key, h.noted, {REFUSED} AS refused \
FROM ({keys}) AS k(row_key) \
CROSS JOIN LATERAL (SELECT * FROM sync.held h \
WHERE h.relation = {relation} AND h.row_key = k.row_key OFFSET 0) h"
)
}
fn held_by_replica(relation: &str, replicas: &str) -> String {
format!(
"SELECT h.replica_id, h.row_key, h.noted, {REFUSED} AS refused \
FROM ({replicas}) AS r(replica_id) \
CROSS JOIN LATERAL (SELECT * FROM sync.held h \
WHERE h.replica_id = r.replica_id AND h.relation = {relation} OFFSET 0) h"
)
}
fn held_by(f: &str, table: &str) -> String {
holds(
"JOIN",
"h",
&format!("{f}.replica_id"),
&literal(table),
&format!("{f}.row_key"),
)
}
pub(crate) fn stale(newest: &str) -> String {
format!("coalesce({newest} > h.noted, false)")
}
fn not_null(alias: &str, columns: &[String]) -> String {
columns
.iter()
.map(|c| format!("{alias}.{} IS NOT NULL", ident(c)))
.collect::<Vec<_>>()
.join(" AND ")
}
const PLAN_TABLES: &str = "
CREATE TEMP TABLE IF NOT EXISTS sync_live (replica_id UUID PRIMARY KEY, fresh BOOLEAN NOT NULL)
ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_batch (id BIGINT PRIMARY KEY) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_changed (
rel INTEGER, relation TEXT NOT NULL, row_key TEXT, noted BIGINT NOT NULL,
PRIMARY KEY (rel, row_key)) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_regrouped (
rel INTEGER, relation TEXT NOT NULL, row_key TEXT, PRIMARY KEY (rel, row_key))
ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_reassigned (
rel INTEGER, relation TEXT NOT NULL, replica_id UUID, PRIMARY KEY (rel, replica_id))
ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_released (
relation TEXT, row_key TEXT, parent INTEGER, parent_relation TEXT, parent_key TEXT)
ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_blocked (
replica_id UUID, rel INTEGER, row_key TEXT, why TEXT NOT NULL, round INTEGER NOT NULL,
PRIMARY KEY (replica_id, rel, row_key)) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_suspect (
replica_id UUID, rel INTEGER, row_key TEXT, round INTEGER NOT NULL,
PRIMARY KEY (replica_id, rel, row_key)) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_kept (
replica_id UUID, rel INTEGER, row_key TEXT, round INTEGER NOT NULL,
sends BOOLEAN NOT NULL, up BOOLEAN NOT NULL, down BOOLEAN NOT NULL, older BOOLEAN NOT NULL,
PRIMARY KEY (replica_id, rel, row_key)) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_sends (
rel INTEGER, row_key TEXT, replica_id UUID, lacks BOOLEAN) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_touched (replica_id UUID PRIMARY KEY) ON COMMIT DELETE ROWS;
CREATE TEMP TABLE IF NOT EXISTS sync_settled (replica_id UUID, relation TEXT, row_key TEXT)
ON COMMIT DELETE ROWS;
";
const PLAN_RESET: &str = "
SET LOCAL work_mem = '64MB';
DELETE FROM sync_live; DELETE FROM sync_batch; DELETE FROM sync_changed;
DELETE FROM sync_regrouped; DELETE FROM sync_reassigned; DELETE FROM sync_released;
DELETE FROM sync_blocked; DELETE FROM sync_suspect; DELETE FROM sync_kept;
DELETE FROM sync_sends; DELETE FROM sync_touched; DELETE FROM sync_settled;
";
pub(crate) fn parse_key(columns: &[Column], text: &str) -> Result<Key, Error> {
let parts: Vec<&str> = text.split('|').collect();
if parts.len() != columns.len() {
return Err(Error::Malformed {
table: String::new(),
reason: format!(
"`{text}` has {} parts, and the key {}",
parts.len(),
columns.len()
),
});
}
columns
.iter()
.zip(parts)
.map(|(column, part)| {
KeyPart::parse(column.key, part).ok_or_else(|| Error::Malformed {
table: String::new(),
reason: format!("`{part}` is no value of the key column `{}`", column.name),
})
})
.collect()
}
fn key_of(row: &serde_json::Value, columns: &[String]) -> Option<String> {
let parts: Option<Vec<String>> = columns
.iter()
.map(|c| match row.get(c)? {
serde_json::Value::String(v) => Some(v.clone()),
serde_json::Value::Number(v) => Some(v.to_string()),
_ => None,
})
.collect();
parts.map(|p| p.join("|"))
}
#[derive(Debug, PartialEq, Eq)]
struct Trigger {
on: Relation,
name: &'static str,
kind: i16,
function: &'static str,
args: Vec<String>,
}
const ROW: i16 = 1;
const BEFORE: i16 = 2;
const INSERT: i16 = 4;
const DELETE: i16 = 8;
const UPDATE: i16 = 16;
const TRUNCATE: i16 = 32;
impl Trigger {
fn ddl(&self) -> String {
let timing = if self.kind & BEFORE != 0 {
"BEFORE"
} else {
"AFTER"
};
let events: Vec<&str> = [
(INSERT, "INSERT"),
(UPDATE, "UPDATE"),
(DELETE, "DELETE"),
(TRUNCATE, "TRUNCATE"),
]
.into_iter()
.filter(|(bit, _)| self.kind & bit != 0)
.map(|(_, event)| event)
.collect();
let each = if self.kind & ROW != 0 {
"ROW"
} else {
"STATEMENT"
};
let args: Vec<String> = self.args.iter().map(|a| literal(a)).collect();
format!(
"CREATE TRIGGER {} {timing} {} ON {} FOR EACH {each} EXECUTE FUNCTION sync.{}({});",
self.name,
events.join(" OR "),
self.on.sql(),
self.function,
args.join(", ")
)
}
}
#[derive(sqlx::FromRow)]
struct LiveTrigger {
schema: String,
table: String,
name: String,
kind: i16,
function: String,
nargs: i16,
args: Vec<u8>,
plain: bool,
}
impl LiveTrigger {
fn on(&self) -> Relation {
Relation {
schema: self.schema.clone(),
name: self.table.clone(),
}
}
fn matches(&self, expected: &Trigger) -> bool {
let args: Vec<String> = self
.args
.split(|b| *b == 0)
.take(usize::try_from(self.nargs).unwrap_or_default())
.map(|a| String::from_utf8_lossy(a).into_owned())
.collect();
self.plain
&& self.kind == expected.kind
&& self.function == expected.function
&& args == expected.args
}
}
async fn live_triggers(conn: &mut Conn) -> Result<Vec<LiveTrigger>, Error> {
Ok(sqlx::query_as(
"SELECT n.nspname::text AS schema, c.relname::text AS table, t.tgname::text AS name, \
t.tgtype AS kind, p.proname::text AS function, t.tgnargs AS nargs, t.tgargs AS args, \
t.tgenabled IN ('O', 'A') AND t.tgqual IS NULL AND t.tgattr::text = '' AS plain \
FROM pg_trigger t \
JOIN pg_class c ON c.oid = t.tgrelid \
JOIN pg_namespace n ON n.oid = c.relnamespace \
JOIN pg_proc p ON p.oid = t.tgfoid \
JOIN pg_namespace pn ON pn.oid = p.pronamespace \
WHERE pn.nspname = 'sync' AND NOT t.tgisinternal AND t.tgparentid = 0",
)
.fetch_all(&mut *conn)
.await?)
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Index {
on: Relation,
columns: Vec<String>,
}
impl Index {
fn ddl(&self) -> String {
let columns: Vec<String> = self.columns.iter().map(|c| ident(c)).collect();
format!(
"CREATE INDEX {} ON {} ({});",
ident(&self.name()),
self.on.sql(),
columns.join(", ")
)
}
fn name(&self) -> String {
let name = format!("{}_{}", self.on.name, self.columns.join("_"));
let apart = format!("{}\0{}", self.on.name, self.columns.join("\0"));
let digest = apart.bytes().fold(0xcbf2_9ce4_8422_2325_u64, |hash, byte| {
(hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
});
let mut cut = name.len().min(41);
while !name.is_char_boundary(cut) {
cut -= 1;
}
format!("{}_sync_{digest:016x}", &name[..cut])
}
fn served(&self, live: &[LiveIndex]) -> bool {
let mut needed: Vec<&str> = self.columns.iter().map(String::as_str).collect();
needed.sort_unstable();
live.iter().any(|index| {
let mut leading: Vec<&str> = index
.columns
.iter()
.take(needed.len())
.map(|c| c.as_deref().unwrap_or_default())
.collect();
leading.sort_unstable();
index.on() == self.on && leading == needed
})
}
}
#[derive(sqlx::FromRow)]
struct LiveIndex {
schema: String,
table: String,
columns: Vec<Option<String>>,
}
impl LiveIndex {
fn on(&self) -> Relation {
Relation {
schema: self.schema.clone(),
name: self.table.clone(),
}
}
}
async fn live_indexes(
conn: &mut Conn,
relations: &BTreeSet<&Relation>,
) -> Result<Vec<LiveIndex>, Error> {
let schemas: Vec<&str> = relations.iter().map(|r| r.schema.as_str()).collect();
let names: Vec<&str> = relations.iter().map(|r| r.name.as_str()).collect();
Ok(sqlx::query_as(
"SELECT n.nspname::text AS schema, c.relname::text AS table, \
array_agg(a.attname::text ORDER BY k.n) AS columns \
FROM unnest($1::text[], $2::text[]) AS r(schema, name) \
JOIN pg_namespace n ON n.nspname = r.schema \
JOIN pg_class c ON c.relnamespace = n.oid AND c.relname = r.name \
JOIN pg_index i ON i.indrelid = c.oid \
JOIN pg_class ic ON ic.oid = i.indexrelid \
JOIN pg_am am ON am.oid = ic.relam \
CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, n) \
LEFT JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum \
WHERE i.indisvalid AND i.indpred IS NULL AND am.amname = 'btree' \
AND k.n <= i.indnkeyatts \
GROUP BY i.indexrelid, n.nspname, c.relname",
)
.bind(&schemas)
.bind(&names)
.fetch_all(&mut *conn)
.await?)
}
async fn migrate(conn: &mut Conn) -> Result<(), Error> {
sqlx::query("SELECT pg_advisory_xact_lock($1)")
.bind(INSTALL_LOCK)
.execute(&mut *conn)
.await?;
let installed = match version(conn).await? {
Some(installed) => installed,
None => {
refuse_foreign_schema(conn).await?;
0
}
};
let from = usize::try_from(installed).unwrap_or(usize::MAX);
if from > MIGRATIONS.len() {
return Err(Error::Misuse(format!(
"the engine's schema is version {installed}, from a later build than this one ({})",
crate::SERVER_SCHEMA_VERSION
)));
}
for step in (installed + 1)..=crate::SERVER_SCHEMA_VERSION {
let script = migration(step).expect("a step of this build");
sqlx::Executor::execute(&mut *conn, script.as_str()).await?;
}
Ok(())
}
async fn refuse_foreign_schema(conn: &mut Conn) -> Result<(), Error> {
let taken: bool =
sqlx::query_scalar("SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'sync')")
.fetch_one(&mut *conn)
.await?;
if taken {
return Err(Error::Misuse(
"the database has a schema named `sync` that is not the engine's: the engine \
keeps its tables in `sync`, so that one needs another name"
.to_owned(),
));
}
Ok(())
}
async fn version(conn: &mut Conn) -> Result<Option<i32>, Error> {
let versioned: bool = sqlx::query_scalar("SELECT to_regclass('sync.version') IS NOT NULL")
.fetch_one(&mut *conn)
.await?;
if !versioned {
return Ok(None);
}
Ok(sqlx::query_scalar("SELECT version FROM sync.version")
.fetch_optional(&mut *conn)
.await?)
}
impl Engine {
pub(crate) async fn new(
conn: &mut Conn,
tables: &[&Table],
replicas: Replicas,
) -> Result<Self, Error> {
crate::row::parity(conn, tables).await?;
let engine = Self::read(conn, tables, replicas).await?;
let temporary: bool =
sqlx::query_scalar("SELECT has_database_privilege(current_database(), 'TEMPORARY')")
.fetch_one(&mut *conn)
.await?;
if !temporary {
return Err(Error::Misuse(
"the engine's role may not create temporary tables, which the relay plans in: \
grant it TEMPORARY on the database"
.to_owned(),
));
}
let live = live_triggers(conn).await?;
let mut wrong = Vec::new();
for expected in engine.triggers_needed() {
match live
.iter()
.find(|t| t.on() == expected.on && t.name == expected.name)
{
Some(t) if t.matches(&expected) => {}
Some(_) => wrong.push(format!("differs, and is: {}", expected.ddl())),
None => wrong.push(format!("is missing: {}", expected.ddl())),
}
}
for stray in engine.strays(&live) {
wrong.push(format!(
"`{}` on {} notes for no table this engine declares: drop it",
stray.name,
stray.on().sql()
));
}
if !wrong.is_empty() {
return Err(Error::Misuse(format!(
"the engine's triggers are not as it needs them — write them as Engine::triggers \
does, in a migration:\n{}",
wrong.join("\n")
)));
}
let missing = engine.indexes_missing(conn).await?;
if !missing.is_empty() {
let ddl: Vec<String> = missing.iter().map(Index::ddl).collect();
return Err(Error::Misuse(format!(
"the planner looks rows up by columns no index leads with — create the indexes \
Engine::indexes prints, in a migration:\n{}",
ddl.join("\n")
)));
}
Ok(engine)
}
pub(crate) async fn install(
conn: &mut Conn,
tables: &[&Table],
replicas: Replicas,
) -> Result<Self, Error> {
migrate(conn).await?;
crate::row::parity(conn, tables).await?;
let engine = Self::read(conn, tables, replicas).await?;
let live = live_triggers(conn).await?;
for stray in engine.strays(&live) {
let drop = format!(
"DROP TRIGGER {} ON {}",
ident(&stray.name),
stray.on().sql()
);
sqlx::Executor::execute(&mut *conn, drop.as_str()).await?;
}
let mut unnoted = false;
for expected in engine.triggers_needed() {
let existing = live
.iter()
.find(|t| t.on() == expected.on && t.name == expected.name);
if existing.is_some_and(|t| t.matches(&expected)) {
continue;
}
if let Some(existing) = existing {
let drop = format!("DROP TRIGGER {} ON {}", expected.name, expected.on.sql());
sqlx::Executor::execute(&mut *conn, drop.as_str()).await?;
unnoted |= !existing.plain;
} else if expected.function == "note_row" {
let has_rows: bool = sqlx::query_scalar(&format!(
"SELECT EXISTS (SELECT 1 FROM {})",
expected.on.sql()
))
.fetch_one(&mut *conn)
.await?;
unnoted |= has_rows;
}
sqlx::Executor::execute(&mut *conn, expected.ddl().as_str()).await?;
}
for index in engine.indexes_missing(conn).await? {
sqlx::Executor::execute(&mut *conn, index.ddl().as_str()).await?;
}
if unnoted {
sqlx::query(&resync_sql(&format!(
"SELECT s.id, s.partition FROM {}",
Self::replicas_of(engine.replicas)
)))
.execute(&mut *conn)
.await?;
}
Ok(engine)
}
pub async fn triggers<P: std::marker::Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
replicas: Replicas,
) -> Result<String, Error> {
let wishes = crate::wish::engines(codecs)?;
let tables: Vec<&Table> = codecs.iter().chain(&wishes).map(|c| &c.table).collect();
let engine = Self::read(conn, &tables, replicas).await?;
Ok(engine
.triggers_needed()
.iter()
.map(Trigger::ddl)
.collect::<Vec<_>>()
.join("\n"))
}
pub async fn indexes<P: std::marker::Sync + 'static>(
conn: &mut Conn,
codecs: &[Codec<P>],
replicas: Replicas,
) -> Result<String, Error> {
let wishes = crate::wish::engines(codecs)?;
let tables: Vec<&Table> = codecs.iter().chain(&wishes).map(|c| &c.table).collect();
let engine = Self::read(conn, &tables, replicas).await?;
Ok(engine
.indexes_missing(conn)
.await?
.iter()
.map(Index::ddl)
.collect::<Vec<_>>()
.join("\n"))
}
fn indexes_needed(&self) -> BTreeSet<Index> {
let mut out = BTreeSet::new();
let mut need = |on: &Relation, columns: Vec<String>| {
out.insert(Index {
on: on.clone(),
columns,
});
};
for plan in &self.plans {
for reference in &plan.references {
need(&plan.relation, reference.columns.clone());
}
match plan.route {
Route::Broadcast => {
if let Some(partition) = self.replicas.partition {
need(&plan.relation, vec![partition.to_owned()]);
}
}
Route::Assigned(column) | Route::Follows(column) => {
need(&plan.relation, vec![column.to_owned()]);
}
Route::Referenced | Route::Grouped { .. } => {}
}
if let Some(g) = &plan.group {
need(&g.members, g.member_row.clone());
need(&g.members, g.group.iter().map(|(m, _)| m.clone()).collect());
need(
&g.assigned,
g.group.iter().map(|(_, a)| a.clone()).collect(),
);
need(&g.assigned, vec![g.assigned_replica.clone()]);
}
}
out
}
async fn indexes_missing(&self, conn: &mut Conn) -> Result<Vec<Index>, Error> {
let needed = self.indexes_needed();
let relations: BTreeSet<&Relation> = needed.iter().map(|i| &i.on).collect();
let live = live_indexes(conn, &relations).await?;
Ok(needed.into_iter().filter(|i| !i.served(&live)).collect())
}
fn triggers_needed(&self) -> Vec<Trigger> {
let partition = self.replicas.partition.unwrap_or_default().to_owned();
let mut out = Vec::new();
let mut groups: BTreeMap<Relation, (String, String, &str)> = BTreeMap::new();
for plan in &self.plans {
let mut args = vec![
plan.table.to_owned(),
partition.clone(),
plan.key.len().to_string(),
];
args.extend(plan.key.iter().map(|c| c.name.to_owned()));
args.push(plan.references.len().to_string());
for reference in &plan.references {
args.push(self.plans[reference.parent].table.to_owned());
args.push(reference.columns.len().to_string());
args.extend(reference.columns.iter().cloned());
}
args.extend(plan.owned.iter().map(|c| (*c).to_owned()));
out.push(Trigger {
on: plan.relation.clone(),
name: "sync_note_row",
kind: ROW | INSERT | UPDATE | DELETE,
function: "note_row",
args,
});
out.push(Trigger {
on: plan.relation.clone(),
name: "sync_refuse_truncate",
kind: BEFORE | TRUNCATE,
function: "refuse_truncate",
args: Vec::new(),
});
if let (Some(g), Route::Grouped { members, assigned }) = (&plan.group, plan.route) {
groups.insert(
g.members.clone(),
(members.to_owned(), g.group[0].0.clone(), "members"),
);
groups.insert(
g.assigned.clone(),
(assigned.to_owned(), g.assigned_replica.clone(), "assigned"),
);
}
}
for (on, (name, column, role)) in groups {
out.push(Trigger {
on,
name: "sync_note_group",
kind: ROW | INSERT | UPDATE | DELETE,
function: "note_group",
args: vec![name, column, role.to_owned(), partition.clone()],
});
}
out
}
fn strays<'a>(&self, live: &'a [LiveTrigger]) -> Vec<&'a LiveTrigger> {
let needed = self.triggers_needed();
live.iter()
.filter(|t| !needed.iter().any(|n| n.on == t.on() && n.name == t.name))
.collect()
}
async fn read(conn: &mut Conn, tables: &[&Table], replicas: Replicas) -> Result<Self, Error> {
let needed = crate::SERVER_SCHEMA_VERSION;
match version(conn).await? {
Some(v) if v == needed => {}
Some(v) if v > needed => {
return Err(Error::Misuse(format!(
"the engine's schema is version {v}, from a later build than this one \
({needed}): run that build, or a later one"
)))
}
v => {
if v.is_none() {
refuse_foreign_schema(conn).await?;
}
return Err(Error::Misuse(format!(
"the engine's schema is {}, and this build needs version {needed}: \
`Server::install` brings it there, or a migration of yours runs the \
engine's steps (the book's \"Upgrading\")",
v.map_or("not installed".to_owned(), |v| format!("version {v}"))
)));
}
}
let tables: Vec<&Table> = tables
.iter()
.copied()
.filter(|t| t.mirror.sent_down())
.collect();
let mut by_name: HashMap<&'static str, usize> = HashMap::new();
for (i, t) in tables.iter().enumerate() {
if by_name.insert(t.name, i).is_some() {
return Err(Error::Misuse(format!(
"two mirrored tables are named `{}`: a replica tells tables apart by name",
t.name
)));
}
}
let foreign_keys: Vec<CatalogForeignKey> =
sqlx::query_as(
"SELECT child_ns.nspname::text, child.relname::text,
parent_ns.nspname::text, parent.relname::text,
ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY AS k(n, o)
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n
ORDER BY k.o),
ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY AS k(n, o)
JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.n
ORDER BY k.o)
FROM pg_constraint c
JOIN pg_class child ON child.oid = c.conrelid
JOIN pg_namespace child_ns ON child_ns.oid = child.relnamespace
JOIN pg_class parent ON parent.oid = c.confrelid
JOIN pg_namespace parent_ns ON parent_ns.oid = parent.relnamespace
WHERE c.contype = 'f' AND c.conparentid = 0
ORDER BY 1, 2, 3, 4, 5, 6",
)
.fetch_all(&mut *conn)
.await?;
let foreign_keys: Vec<ForeignKey> = foreign_keys
.into_iter()
.map(|(cs, c, ps, p, cc, pc)| {
(
Relation {
schema: cs,
name: c,
},
Relation {
schema: ps,
name: p,
},
cc,
pc,
)
})
.collect();
let mut plans: Vec<Plan> = Vec::with_capacity(tables.len());
for t in &tables {
let relation = Relation {
schema: t.schema.to_owned(),
name: t.name.to_owned(),
};
let key: Vec<Column> = t
.key
.iter()
.map(|k| {
t.columns
.iter()
.find(|c| c.name == *k)
.copied()
.ok_or_else(|| {
Error::Misuse(format!(
"`{}` is keyed by `{k}`, which it does not declare",
t.name
))
})
})
.collect::<Result<_, _>>()?;
let declared: BTreeSet<&str> = t.key.iter().copied().collect();
let primary = crate::dialect::primary_key(conn, t.schema, t.name).await?;
if primary.iter().map(String::as_str).collect::<BTreeSet<_>>() != declared {
return Err(Error::Misuse(format!(
"`{}.{}` is mirrored keyed by {:?}, which is not its primary key",
t.schema, t.name, t.key
)));
}
plans.push(Plan {
table: t.name,
active: t.active.map_or_else(|| relation.sql(), str::to_owned),
relation,
key,
route: t.route,
references: Vec::new(),
group: None,
owned: t
.columns
.iter()
.filter(|c| c.replica_owned)
.map(|c| c.name)
.collect(),
columns: t.columns.iter().map(|c| c.name).collect(),
size: String::new(),
encoding: t.encoding,
});
}
let mirrored = |relation: &Relation| {
by_name
.get(relation.name.as_str())
.copied()
.filter(|&i| plans[i].relation == *relation)
};
let mut references: Vec<(usize, Reference)> = Vec::new();
for (child, parent, child_columns, parent_columns) in &foreign_keys {
let (Some(c), Some(p)) = (mirrored(child), mirrored(parent)) else {
continue;
};
if c == p {
return Err(Error::Misuse(format!(
"`{}` points at itself: the engine cannot order a table's rows among themselves",
child.name
)));
}
let columns: Option<Vec<String>> = plans[p]
.key
.iter()
.map(|k| {
parent_columns
.iter()
.position(|pc| pc == k.name)
.map(|i| child_columns[i].clone())
})
.collect();
let Some(columns) = columns.filter(|_| parent_columns.len() == plans[p].key.len())
else {
return Err(Error::Misuse(format!(
"`{}` points at `{}` by {parent_columns:?}, which is not its key: the engine \
sends a parent by its key",
child.name, parent.name
)));
};
references.push((c, Reference { parent: p, columns }));
}
for (c, reference) in references {
plans[c].references.push(reference);
}
for plan in &mut plans {
match plan.route {
Route::Follows(column) => {
if !plan
.references
.iter()
.any(|r| r.columns == [column.to_owned()])
{
return Err(Error::Misuse(format!(
"`{}` follows `{column}`, which is not a key into a mirrored table",
plan.table
)));
}
}
Route::Grouped { members, assigned } => {
plan.group = Some(group_of(
&foreign_keys,
plan,
members,
assigned,
replicas.table,
)?);
}
_ => {}
}
}
let partition = replicas.partition.unwrap_or_default();
let replicas_typed = typed_columns(conn, replicas.relation).await?;
let type_of = |columns: &[(String, String)], column: &str| {
columns
.iter()
.find(|(c, _)| c == column)
.map(|(_, t)| t.clone())
};
if replicas_typed.is_empty() {
return Err(Error::Misuse(format!(
"the replicas' relation `{}` is no table or view of the database",
replicas.relation
)));
}
for column in ["id", partition].into_iter().filter(|c| !c.is_empty()) {
if type_of(&replicas_typed, column).as_deref() != Some("uuid") {
return Err(Error::Misuse(format!(
"the replicas' relation `{}` has no uuid column `{column}`",
replicas.relation
)));
}
}
readable(
conn,
&format!("SELECT s.id FROM {} LIMIT 0", Self::replicas_of(replicas)),
&format!(
"the replicas, `{}` where `{}`",
replicas.relation, replicas.live
),
)
.await?;
let mut noted: Vec<Relation> = plans.iter().map(|p| p.relation.clone()).collect();
noted.extend(
plans
.iter()
.filter_map(|p| p.group.as_ref())
.flat_map(|g| [g.members.clone(), g.assigned.clone()]),
);
for relation in ¬ed {
let columns = typed_columns(conn, &relation.sql()).await?;
if type_of(&columns, partition).is_some_and(|t| t != "uuid") {
return Err(Error::Misuse(format!(
"{} has a column `{partition}` that is not a uuid: the replicas are \
partitioned by the uuid of that name",
relation.sql()
)));
}
}
for plan in &mut plans {
readable(
conn,
&format!("SELECT 1 FROM {} LIMIT 0", plan.active),
&format!("the active rows of `{}`, `{}`", plan.table, plan.active),
)
.await?;
let columns = typed_columns(conn, &plan.relation.sql()).await?;
plan.size = plan
.columns
.iter()
.map(|c| match type_of(&columns, c).as_deref() {
Some("text" | "bytea" | "character varying" | "character") => {
format!("coalesce(octet_length(t.{}), 0)", ident(c))
}
_ => format!("coalesce(octet_length(t.{}::text), 0)", ident(c)),
})
.chain(["0".to_owned()])
.collect::<Vec<_>>()
.join(" + ");
let has = |column: &str| type_of(&columns, column).is_some();
let missing = match plan.route {
Route::Broadcast => replicas.partition.filter(|p| !has(p)),
Route::Assigned(column) if !has(column) => Some(column),
_ => None,
};
if let Some(column) = missing {
return Err(Error::Misuse(format!(
"`{}` is sent by `{column}`, which it does not have",
plan.table
)));
}
}
let order = topological(&plans)?;
Ok(Self {
plans,
by_name,
order,
replicas,
})
}
fn declared(&self) -> String {
use std::fmt::Write as _;
let r = &self.replicas;
let mut out = format!(
"replicas {} ({}) live {} partition {:?}\n",
r.relation, r.table, r.live, r.partition
);
for &i in &self.order {
let p = &self.plans[i];
let key: Vec<String> = p
.key
.iter()
.map(|c| format!("{} {}", c.name, KeyPart::sql_type(c.key)))
.collect();
let references: Vec<String> = p
.references
.iter()
.map(|r| format!("{} {:?}", self.plans[r.parent].table, r.columns))
.collect();
let _ = writeln!(
out,
"{} {} active {} key {key:?} {:?} references {references:?} group {:?} columns {:?} \
as {}",
p.table,
p.relation.sql(),
p.active,
p.route,
p.group,
p.columns,
p.encoding,
);
}
out
}
pub(crate) fn index(&self, table: &str) -> Option<usize> {
self.by_name.get(table).copied()
}
pub(crate) fn table(&self, index: usize) -> &'static str {
self.plans[index].table
}
pub(crate) fn replicas_sql(&self) -> String {
"(SELECT replica_id AS id, partition_id AS partition FROM sync.replicas WHERE live) AS s"
.to_owned()
}
fn replicas_of(replicas: Replicas) -> String {
let partition = replicas.partition.map_or("NULL::uuid".to_owned(), ident);
format!(
"(SELECT DISTINCT ON (id) id, {partition} AS partition FROM {} WHERE {} \
ORDER BY id, {partition}) AS s",
replicas.relation, replicas.live
)
}
pub(crate) async fn is_replica(&self, conn: &mut Conn, replica: Uuid) -> Result<bool, Error> {
Ok(sqlx::query_scalar(&format!(
"SELECT EXISTS (SELECT 1 FROM {} WHERE id = $1)",
self.replicas.relation
))
.bind(replica)
.fetch_one(&mut *conn)
.await?)
}
pub(crate) async fn observe(&self, conn: &mut Conn) -> Result<(), Error> {
sqlx::query(&format!(
"WITH now AS (SELECT s.id, s.partition FROM {}), \
seen AS (SELECT replica_id, partition_id FROM sync.replicas WHERE live), \
arrived AS (SELECT n.id, n.partition FROM now n \
WHERE NOT EXISTS (SELECT 1 FROM seen s WHERE s.replica_id = n.id \
AND s.partition_id IS NOT DISTINCT FROM n.partition)), \
gone AS (SELECT s.replica_id FROM seen s \
WHERE NOT EXISTS (SELECT 1 FROM now n WHERE n.id = s.replica_id)), \
provisioned AS ({}), \
forgotten AS (DELETE FROM sync.held h USING gone g WHERE h.replica_id = g.replica_id), \
untaken AS (DELETE FROM sync.gone t USING gone g WHERE t.replica_id = g.replica_id), \
dropped AS (DELETE FROM sync.outbox o USING gone g WHERE o.replica_id = g.replica_id), \
unchained AS (DELETE FROM sync.chains c USING gone g WHERE c.replica_id = g.replica_id), \
came AS (INSERT INTO sync.replicas AS r (replica_id, live, partition_id) \
SELECT id, true, partition FROM arrived \
ON CONFLICT (replica_id) DO UPDATE SET live = true, partition_id = EXCLUDED.partition_id) \
UPDATE sync.replicas r SET live = false, partition_id = NULL \
FROM gone g WHERE r.replica_id = g.replica_id",
Self::replicas_of(self.replicas),
resync_sql("SELECT id, partition FROM arrived")
))
.execute(&mut *conn)
.await?;
self.redeclared(conn).await
}
async fn redeclared(&self, conn: &mut Conn) -> Result<(), Error> {
let declared = self.declared();
let before: Option<Option<String>> = sqlx::query_scalar(
"UPDATE sync.version v SET declared = encode(sync.digest($1), 'hex') FROM sync.version was \
WHERE v.declared IS DISTINCT FROM encode(sync.digest($1), 'hex') RETURNING was.declared",
)
.bind(&declared)
.fetch_optional(&mut *conn)
.await?;
let Some(Some(_)) = before else {
return Ok(());
};
tracing::warn!("sync: what places the rows changed since the engine last ran — every replica is provisioned again");
let tables: Vec<&str> = self.plans.iter().map(|p| p.table).collect();
for sql in [
"DELETE FROM sync.held WHERE relation <> ALL($1)",
"DELETE FROM sync.gone WHERE relation <> ALL($1)",
"DELETE FROM sync.quarantine WHERE relation <> ALL($1)",
"UPDATE sync.notes SET done_at = now() \
WHERE done_at IS NULL AND kind IN ('row', 'retry') AND relation <> ALL($1)",
] {
sqlx::query(sql).bind(&tables).execute(&mut *conn).await?;
}
sqlx::query(&resync_sql(&format!(
"SELECT s.id, s.partition FROM {}",
Self::replicas_of(self.replicas)
)))
.execute(&mut *conn)
.await?;
Ok(())
}
fn same_partition(&self) -> String {
match self.replicas.partition {
Some(column) => format!("s.partition = t.{}", ident(column)),
None => "true".to_owned(),
}
}
pub(crate) fn wanted_sql(&self, i: usize, filter: &str) -> Option<String> {
let plan = &self.plans[i];
let key = key_sql("t", &plan.key.iter().map(|c| c.name).collect::<Vec<_>>());
let replicas = self.replicas_sql();
let same_partition = self.same_partition();
let view = &plan.active;
Some(match plan.route {
Route::Referenced | Route::Follows(_) => return None,
Route::Broadcast => format!(
"SELECT s.id AS replica_id, {key} AS key FROM {view} t \
JOIN {replicas} ON {same_partition} WHERE {filter}"
),
Route::Assigned(column) => format!(
"SELECT s.id AS replica_id, {key} AS key FROM {view} t \
JOIN {replicas} ON s.id = t.{} WHERE {filter}",
ident(column)
),
Route::Grouped { .. } => {
let g = plan.group.as_ref()?;
let member_row: Vec<String> = g
.member_row
.iter()
.zip(&plan.key)
.map(|(m, k)| format!("m.{} = t.{}", ident(m), ident(k.name)))
.collect();
let group: Vec<String> = g
.group
.iter()
.map(|(m, a)| format!("a.{} = m.{}", ident(a), ident(m)))
.collect();
format!(
"SELECT DISTINCT s.id AS replica_id, {key} AS key FROM {view} t \
JOIN {} m ON {} JOIN {} a ON {} \
JOIN {replicas} ON s.id = a.{} WHERE {filter}",
g.members.sql(),
member_row.join(" AND "),
g.assigned.sql(),
group.join(" AND "),
ident(&g.assigned_replica),
)
}
})
}
pub(crate) async fn wanted(
&self,
conn: &mut Conn,
i: usize,
keys: Option<&[String]>,
replicas: Option<&[Uuid]>,
) -> Result<BTreeSet<(Uuid, String)>, Error> {
let plan = &self.plans[i];
let names: Vec<&str> = plan.key.iter().map(|c| c.name).collect();
let mut filter = vec!["true".to_owned()];
if keys.is_some() {
filter.push(key_in("t", &names, &plan.key, "$1"));
}
if replicas.is_some() {
filter.push(format!("s.id = ANY(${})", 1 + usize::from(keys.is_some())));
}
let Some(sql) = self.wanted_sql(i, &filter.join(" AND ")) else {
return Ok(BTreeSet::new());
};
let mut query = sqlx::query_as::<_, (Uuid, String)>(&sql);
if let Some(keys) = keys {
query = query.bind(keys);
}
if let Some(replicas) = replicas {
query = query.bind(replicas);
}
Ok(query.fetch_all(&mut *conn).await?.into_iter().collect())
}
pub(crate) async fn plan(&self, conn: &mut Conn, notes: &[Note]) -> Result<Delta, Error> {
let delta = Delta {
notes: notes.to_vec(),
..Delta::default()
};
self.plan_around(conn, delta).await
}
async fn plan_around(&self, conn: &mut Conn, mut delta: Delta) -> Result<Delta, Error> {
let made: bool = sqlx::query_scalar("SELECT to_regclass('pg_temp.sync_kept') IS NOT NULL")
.fetch_one(&mut *conn)
.await?;
if !made {
sqlx::Executor::execute(&mut *conn, "SAVEPOINT sync_temp_buffers").await?;
let end = match sqlx::Executor::execute(&mut *conn, "SET temp_buffers = '128MB'").await
{
Ok(_) => "RELEASE SAVEPOINT sync_temp_buffers",
Err(_) => "ROLLBACK TO SAVEPOINT sync_temp_buffers",
};
sqlx::Executor::execute(&mut *conn, end).await?;
sqlx::Executor::execute(&mut *conn, PLAN_TABLES).await?;
}
sqlx::Executor::execute(&mut *conn, PLAN_RESET).await?;
let provisioned: Vec<Uuid> = delta
.notes
.iter()
.filter_map(|n| match n.change {
Change::Resync { replica } => Some(replica),
_ => None,
})
.collect();
delta.snapshots = sqlx::query_scalar(&format!(
"WITH live AS (INSERT INTO sync_live SELECT s.id, s.id = ANY($1) FROM {} \
RETURNING replica_id, fresh) \
SELECT replica_id FROM live WHERE fresh",
self.replicas_sql()
))
.bind(&provisioned)
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect();
let mut asked = self.note(conn, &delta.notes).await?;
asked.fresh = !delta.snapshots.is_empty();
let mut rows = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
for ((replica, i, key), why) in &delta.blocked {
rows.0.push(*replica);
rows.1.push(i32::try_from(*i).unwrap_or(i32::MAX));
rows.2.push(key.as_str());
rows.3.push(why.as_str());
}
sqlx::query(
"INSERT INTO sync_blocked (replica_id, rel, row_key, why, round) \
SELECT *, 0 FROM unnest($1::uuid[], $2::int4[], $3::text[], $4::text[])",
)
.bind(&rows.0)
.bind(&rows.1)
.bind(&rows.2)
.bind(&rows.3)
.execute(&mut *conn)
.await?;
analyze(
conn,
"sync_live, sync_batch, sync_changed, sync_regrouped, sync_reassigned, \
sync_released, sync_blocked",
)
.await?;
let mut blocked_from = 0;
loop {
blocked_from = self.spread_blocked(conn, blocked_from).await?;
self.keep_sends(conn, &asked).await?;
self.question(conn, &asked).await?;
self.keep_all(conn, &asked).await?;
if !self.conflicts(conn, &asked, blocked_from).await? {
break;
}
sqlx::Executor::execute(
&mut *conn,
"DELETE FROM sync_suspect; DELETE FROM sync_kept;",
)
.await?;
}
self.touch(conn).await?;
self.settled(conn, &asked).await?;
let blocked: Vec<(Uuid, i32, String, String)> = sqlx::query_as(
"SELECT b.replica_id, b.rel, b.row_key, b.why FROM sync_blocked b \
JOIN sync_live USING (replica_id)",
)
.fetch_all(&mut *conn)
.await?;
delta.blocked = blocked
.into_iter()
.map(|(r, i, k, why)| ((r, usize::try_from(i).unwrap_or(usize::MAX), k), why))
.collect();
Ok(delta)
}
async fn note(&self, conn: &mut Conn, notes: &[Note]) -> Result<Asked, Error> {
let ids: Vec<i64> = notes.iter().map(|n| n.id).collect();
let mut pointed: HashMap<i64, Vec<(String, String)>> = HashMap::new();
let rows: Vec<(i64, String, String)> =
sqlx::query_as("SELECT note, relation, row_key FROM sync.pointed WHERE note = ANY($1)")
.bind(&ids)
.fetch_all(&mut *conn)
.await?;
for (note, table, key) in rows {
pointed.entry(note).or_default().push((table, key));
}
let mut changed: BTreeMap<(usize, String), i64> = BTreeMap::new();
let mut released: BTreeSet<(usize, String, usize, String)> = BTreeSet::new();
let mut regrouped: BTreeSet<(usize, String)> = BTreeSet::new();
let mut reassigned: BTreeSet<(usize, Uuid)> = BTreeSet::new();
for note in notes {
match ¬e.change {
Change::Row { table, key, retry } => {
let i = self.index(table).ok_or_else(|| {
Error::Misuse(format!(
"a change to `{table}`, which no row declares: its trigger outlived its declaration"
))
})?;
let of_table = |e| match e {
Error::Malformed { reason, .. } => Error::Malformed {
table: table.clone(),
reason,
},
other => other,
};
let key = key_text(&parse_key(&self.plans[i].key, key).map_err(of_table)?);
for (parent, at) in pointed.get(¬e.id).into_iter().flatten() {
let Some(p) = self.index(parent) else {
continue;
};
let at = key_text(&parse_key(&self.plans[p].key, at).map_err(of_table)?);
released.insert((i, key.clone(), p, at));
}
let newest = changed.entry((i, key)).or_default();
if !retry {
*newest = (*newest).max(note.id);
}
}
Change::Members { table, member } => {
for (i, g) in self.groups_through(table, |g| &g.members) {
if let Some(key) = key_of(member, &g.member_row) {
let key = key_text(&parse_key(&self.plans[i].key, &key)?);
regrouped.insert((i, key));
}
}
}
Change::Assigned { table, replica } => {
for (i, _) in self.groups_through(table, |g| &g.assigned) {
reassigned.insert((i, *replica));
}
}
Change::Resync { .. } => {}
}
}
let rel = |i: usize| i32::try_from(i).unwrap_or(i32::MAX);
let name = |i: usize| self.plans[i].table;
sqlx::query("INSERT INTO sync_batch SELECT DISTINCT unnest($1::int8[])")
.bind(&ids)
.execute(&mut *conn)
.await?;
let (keys, noted): (Vec<_>, Vec<i64>) = changed.into_iter().unzip();
sqlx::query(
"INSERT INTO sync_changed \
SELECT * FROM unnest($1::int4[], $2::text[], $3::text[], $4::int8[])",
)
.bind(keys.iter().map(|(i, _)| rel(*i)).collect::<Vec<_>>())
.bind(keys.iter().map(|(i, _)| name(*i)).collect::<Vec<_>>())
.bind(keys.iter().map(|(_, k)| k.as_str()).collect::<Vec<_>>())
.bind(¬ed)
.execute(&mut *conn)
.await?;
sqlx::query(
"INSERT INTO sync_released \
SELECT * FROM unnest($1::text[], $2::text[], $3::int4[], $4::text[], $5::text[])",
)
.bind(released.iter().map(|r| name(r.0)).collect::<Vec<_>>())
.bind(released.iter().map(|r| r.1.as_str()).collect::<Vec<_>>())
.bind(released.iter().map(|r| rel(r.2)).collect::<Vec<_>>())
.bind(released.iter().map(|r| name(r.2)).collect::<Vec<_>>())
.bind(released.iter().map(|r| r.3.as_str()).collect::<Vec<_>>())
.execute(&mut *conn)
.await?;
sqlx::query(
"INSERT INTO sync_regrouped SELECT * FROM unnest($1::int4[], $2::text[], $3::text[])",
)
.bind(regrouped.iter().map(|(i, _)| rel(*i)).collect::<Vec<_>>())
.bind(regrouped.iter().map(|(i, _)| name(*i)).collect::<Vec<_>>())
.bind(
regrouped
.iter()
.map(|(_, k)| k.as_str())
.collect::<Vec<_>>(),
)
.execute(&mut *conn)
.await?;
sqlx::query(
"INSERT INTO sync_reassigned SELECT * FROM unnest($1::int4[], $2::text[], $3::uuid[])",
)
.bind(reassigned.iter().map(|(i, _)| rel(*i)).collect::<Vec<_>>())
.bind(reassigned.iter().map(|(i, _)| name(*i)).collect::<Vec<_>>())
.bind(reassigned.iter().map(|(_, r)| *r).collect::<Vec<_>>())
.execute(&mut *conn)
.await?;
Ok(Asked {
changed: keys.iter().map(|(i, _)| *i).collect(),
regrouped: regrouped.iter().map(|(i, _)| *i).collect(),
reassigned: reassigned.iter().map(|(i, _)| *i).collect(),
released: !released.is_empty(),
fresh: false,
})
}
async fn spread_blocked(&self, conn: &mut Conn, from: i32) -> Result<i32, Error> {
let mut round = from;
loop {
let parents = self.frontier(conn, "sync_blocked", round).await?;
if parents.is_empty() {
return Ok(round);
}
for (c, reference) in self
.references()
.filter(|(_, r)| parents.contains(&r.parent))
{
let child = &self.plans[c];
let select = format!(
"WITH f AS MATERIALIZED ( \
SELECT b.replica_id, {} AS row_key, \
format('it points at `%s` %s, which cannot go: %s', {}, b.row_key, b.why) AS why \
FROM sync_blocked b JOIN {} t ON {} \
WHERE b.rel = {} AND b.round = $1 - 1) \
SELECT f.replica_id, {c}, f.row_key, f.why FROM f {}",
self.key_of_row(c),
literal(self.plans[reference.parent].table),
child.relation.sql(),
self.points_at(reference, "b.row_key"),
reference.parent,
held_by("f", child.table),
);
self.add(conn, Set::Blocked, &select, round + 1).await?;
}
for (c, parent) in self
.pointing()
.into_iter()
.filter(|(_, p)| parents.contains(p))
{
let select = format!(
"SELECT b.replica_id, {c}, h.row_key, \
format('it points at `%s` %s, which cannot go: %s', {}, b.row_key, b.why) \
FROM sync_blocked b {} \
WHERE b.rel = {parent} AND b.round = $1 - 1",
literal(self.plans[parent].table),
self.held_older(c, parent, "b.replica_id", "b.row_key"),
);
self.add(conn, Set::Blocked, &select, round + 1).await?;
}
round += 1;
}
}
async fn question(&self, conn: &mut Conn, asked: &Asked) -> Result<(), Error> {
for i in (0..self.plans.len()).filter(|&i| self.wanted_sql(i, "true").is_none()) {
if let Some(held) = self.held_asked(i, asked) {
let select =
format!("SELECT h.replica_id, {i}, h.row_key, NULL FROM ({held}) AS h");
self.add(conn, Set::Suspect, &select, 0).await?;
}
}
if asked.released {
self.add(
conn,
Set::Suspect,
&format!(
"SELECT h.replica_id, x.parent, x.parent_key, NULL FROM sync_released x {} {}",
holders("x.relation", "x.row_key"),
holds(
"JOIN",
"h",
"c.replica_id",
"x.parent_relation",
"x.parent_key"
)
),
0,
)
.await?;
}
for i in self.present(conn, "sync_blocked").await? {
let select = format!(
"SELECT b.replica_id, b.rel, b.row_key, NULL FROM sync_blocked b {} WHERE b.rel = {i}",
held_by("b", self.plans[i].table)
);
self.add(conn, Set::Suspect, &select, 0).await?;
}
for (f, column, p) in self
.followers()
.filter(|(f, _, _)| asked.changed.contains(f))
{
for c in self
.pointing()
.into_iter()
.filter(|(_, parent)| *parent == p)
.map(|(c, _)| c)
{
let select = format!(
"SELECT DISTINCT h.replica_id, {p}, pp.row_key, NULL \
FROM sync_changed x JOIN {} t ON {} \
JOIN sync.pointed pp ON pp.relation = {} AND pp.row_key = t.{}::text \
JOIN sync.notes n ON n.id = pp.note AND n.relation = {} \
AND n.id NOT IN (SELECT id FROM sync_batch) \
JOIN sync.held h ON h.relation = n.relation AND h.row_key = n.row_key \
AND h.noted < n.id \
JOIN sync_live l ON l.replica_id = h.replica_id AND NOT l.fresh \
WHERE x.rel = {f}",
self.plans[f].relation.sql(),
self.row_is(f, "x.row_key"),
literal(self.plans[p].table),
ident(column),
literal(self.plans[c].table),
);
self.add(conn, Set::Suspect, &select, 0).await?;
}
}
let mut round = 0;
loop {
let from = self.frontier(conn, "sync_suspect", round).await?;
if from.is_empty() {
break;
}
for (c, reference) in self.references().filter(|(c, _)| from.contains(c)) {
let p = reference.parent;
let ruled = self
.wanted_sql(
p,
&self.key_among(p, "SELECT replica_id, row_key FROM g", true),
)
.map_or(String::new(), |w| {
format!(" EXCEPT SELECT w.replica_id, w.key FROM ({w}) w")
});
let select = format!(
"WITH f AS MATERIALIZED ( \
SELECT d.replica_id, {} AS row_key FROM sync_suspect d JOIN {} t ON {} \
WHERE d.rel = {c} AND d.round = $1 - 1 AND {}), \
g AS MATERIALIZED (SELECT DISTINCT f.replica_id, f.row_key FROM f {}) \
SELECT q.replica_id, {p}, q.row_key, NULL FROM (TABLE g{ruled}) AS q",
key_sql("t", &reference.columns),
self.plans[c].relation.sql(),
self.row_is(c, "d.row_key"),
not_null("t", &reference.columns),
held_by("f", self.plans[p].table),
);
self.add(conn, Set::Suspect, &select, round + 1).await?;
}
for (c, parent) in self
.pointing()
.into_iter()
.filter(|(c, _)| from.contains(c))
{
let select = format!(
"SELECT d.replica_id, {parent}, p.row_key, NULL FROM sync_suspect d {} {} \
WHERE d.rel = {c} AND d.round = $1 - 1",
self.pointed_by_held(c, parent, "d"),
holds(
"JOIN",
"h",
"d.replica_id",
&literal(self.plans[parent].table),
"p.row_key"
),
);
self.add(conn, Set::Suspect, &select, round + 1).await?;
}
for (f, column, p) in self.followers().filter(|(_, _, p)| from.contains(p)) {
let select = format!(
"WITH f AS MATERIALIZED ( \
SELECT d.replica_id, {} AS row_key FROM sync_suspect d JOIN {} t ON {} \
WHERE d.rel = {p} AND d.round = $1 - 1) \
SELECT f.replica_id, {f}, f.row_key, NULL FROM f {}",
self.key_of_row(f),
self.plans[f].relation.sql(),
key_eq("t", &[column], &self.plans[p].key, "d.row_key"),
held_by("f", self.plans[f].table),
);
self.add(conn, Set::Suspect, &select, round + 1).await?;
}
round += 1;
}
analyze(conn, "sync_suspect").await
}
async fn keep_all(&self, conn: &mut Conn, asked: &Asked) -> Result<(), Error> {
let doubted = self.present(conn, "sync_suspect").await?;
for &i in &doubted {
let filter = self.key_among(
i,
&format!("SELECT replica_id, row_key FROM sync_suspect WHERE rel = {i}"),
true,
);
if let Some(wanted) = self.wanted_sql(i, &filter) {
let source = format!("SELECT w.replica_id, w.key FROM ({wanted}) w");
self.keep(conn, i, 0, &source).await?;
}
}
for (c, reference) in self.references() {
if let Some(sends) = self.asked_sends(c, asked, ¬_null("t", &reference.columns)) {
let source = format!(
"SELECT a.replica_id, {} FROM ({sends}) a JOIN {} t ON {} WHERE {}",
key_sql("t", &reference.columns),
self.plans[c].relation.sql(),
self.row_is(c, "a.row_key"),
not_null("t", &reference.columns),
);
self.keep(conn, reference.parent, 0, &source).await?;
}
}
for (f, column, p) in self.followers() {
if let Some(sends) = self.asked_sends(p, asked, "true") {
let source = format!(
"SELECT a.replica_id, {} FROM ({sends}) a JOIN {} t ON {} WHERE a.lacks",
self.key_of_row(f),
self.plans[f].active,
key_eq("t", &[column], &self.plans[p].key, "a.row_key"),
);
self.keep(conn, f, 0, &source).await?;
}
}
for (c, reference) in self
.references()
.filter(|(_, r)| doubted.contains(&r.parent))
{
let source = format!(
"WITH f AS MATERIALIZED ( \
SELECT d.replica_id, {} AS row_key, d.row_key AS parent FROM sync_suspect d \
JOIN {} t ON {} WHERE d.rel = {}) \
SELECT f.replica_id, f.parent FROM f {} \
WHERE NOT EXISTS (SELECT 1 FROM sync_suspect x WHERE x.replica_id = f.replica_id \
AND x.rel = {c} AND x.row_key = f.row_key)",
self.key_of_row(c),
self.plans[c].relation.sql(),
self.points_at(reference, "d.row_key"),
reference.parent,
held_by("f", self.plans[c].table),
);
self.keep(conn, reference.parent, 0, &source).await?;
}
for (f, column, p) in self.followers() {
let parent = literal(self.plans[p].table);
let outside = format!(
"NOT EXISTS (SELECT 1 FROM sync_suspect x WHERE x.replica_id = hp.replica_id \
AND x.rel = {p} AND x.row_key = hp.row_key)"
);
let mut sources = Vec::new();
if doubted.contains(&f) {
sources.push(format!(
"SELECT d.replica_id, d.row_key FROM sync_suspect d JOIN {} t ON {} {} \
WHERE d.rel = {f} AND {outside}",
self.plans[f].active,
self.row_is(f, "d.row_key"),
holds(
"JOIN",
"hp",
"d.replica_id",
&parent,
&format!("t.{}::text", ident(column))
),
));
}
if asked.changed.contains(&f) {
sources.push(format!(
"SELECT c.replica_id, {} FROM {} t {} \
WHERE {} AND {}",
self.key_of_row(f),
self.plans[f].active,
holders(&parent, &format!("t.{}::text", ident(column))),
self.key_among(
f,
&format!("SELECT row_key FROM sync_changed WHERE rel = {f}"),
false,
),
outside
.replace("hp.replica_id", "c.replica_id")
.replace("hp.row_key", &format!("t.{}::text", ident(column))),
));
}
for source in sources {
self.keep(conn, f, 0, &source).await?;
}
}
let mut round = 0;
loop {
let from = self.frontier(conn, "sync_kept", round).await?;
if from.is_empty() {
round += 1;
if doubted.is_empty() || !self.keep_older(conn, round).await? {
return analyze(conn, "sync_kept").await;
}
continue;
}
for (c, reference) in self.references().filter(|(c, _)| from.contains(c)) {
let source = format!(
"SELECT k.replica_id, {} FROM sync_kept k JOIN {} t ON {} \
WHERE k.rel = {c} AND k.round = $1 - 1 AND k.up AND {} \
AND (k.sends OR NOT EXISTS ( \
SELECT 1 FROM sync.held h JOIN sync.notes n ON n.kind = 'row' \
AND n.relation = h.relation AND n.row_key = h.row_key AND n.id > h.noted \
WHERE h.replica_id = k.replica_id AND h.relation = {} \
AND h.row_key = k.row_key))",
key_sql("t", &reference.columns),
self.plans[c].relation.sql(),
self.row_is(c, "k.row_key"),
not_null("t", &reference.columns),
literal(self.plans[c].table),
);
self.keep(conn, reference.parent, round + 1, &source)
.await?;
}
for (c, parent) in self
.pointing()
.into_iter()
.filter(|(c, _)| from.contains(c))
{
let source = format!(
"SELECT k.replica_id, p.row_key FROM sync_kept k {} \
WHERE k.rel = {c} AND k.round = $1 - 1 AND k.up AND NOT k.sends",
self.pointed_by_held(c, parent, "k"),
);
self.keep_for_older(conn, parent, round + 1, &source)
.await?;
}
for (f, column, p) in self.followers().filter(|(_, _, p)| from.contains(p)) {
let source = format!(
"SELECT k.replica_id, {key} FROM sync_kept k JOIN {} t ON {} \
WHERE k.rel = {p} AND k.round = $1 - 1 AND k.down \
AND (NOT k.older OR EXISTS (SELECT 1 FROM sync.held h \
WHERE h.replica_id = k.replica_id \
AND h.relation = {} AND h.row_key = {key}))",
self.plans[f].active,
key_eq("t", &[column], &self.plans[p].key, "k.row_key"),
literal(self.plans[f].table),
key = self.key_of_row(f),
);
self.keep(conn, f, round + 1, &source).await?;
}
round += 1;
}
}
async fn keep_older(&self, conn: &mut Conn, round: i32) -> Result<bool, Error> {
let mut kept = 0;
for (c, parent) in self.pointing() {
let source = format!(
"SELECT d.replica_id, d.row_key FROM sync_suspect d {} \
WHERE d.rel = {parent} \
AND (NOT EXISTS (SELECT 1 FROM sync_suspect x WHERE x.replica_id = h.replica_id \
AND x.rel = {c} AND x.row_key = h.row_key) \
OR EXISTS (SELECT 1 FROM sync_kept x WHERE x.replica_id = h.replica_id \
AND x.rel = {c} AND x.row_key = h.row_key AND NOT x.sends))",
self.held_older(c, parent, "d.replica_id", "d.row_key"),
);
kept += self.keep_for_older(conn, parent, round, &source).await?;
}
Ok(kept > 0)
}
fn pointed_by_held(&self, c: usize, parent: usize, alias: &str) -> String {
let child = literal(self.plans[c].table);
format!(
"{} CROSS JOIN LATERAL (SELECT min(n.id) AS id FROM sync.notes n \
WHERE n.kind = 'row' AND n.relation = {child} \
AND n.row_key = {alias}.row_key AND n.id > held.noted) after \
JOIN sync.pointed p ON p.note = after.id AND p.relation = {}",
holds(
"JOIN",
"held",
&format!("{alias}.replica_id"),
&child,
&format!("{alias}.row_key")
),
literal(self.plans[parent].table),
)
}
fn pointing(&self) -> BTreeSet<(usize, usize)> {
self.references().map(|(c, r)| (c, r.parent)).collect()
}
fn held_older(&self, c: usize, parent: usize, replica: &str, text: &str) -> String {
let child = literal(self.plans[c].table);
format!(
"JOIN sync.pointed p ON p.relation = {} AND p.row_key = {text} \
JOIN sync.notes n ON n.id = p.note AND n.relation = {child} \
AND n.id NOT IN (SELECT id FROM sync_batch) \
{} \
JOIN LATERAL (SELECT min(m.id) AS id FROM sync.notes m \
WHERE m.kind = 'row' AND m.relation = {child} AND m.row_key = n.row_key \
AND m.id > h.noted) first ON first.id = n.id",
literal(self.plans[parent].table),
holds("JOIN", "h", replica, &child, "n.row_key"),
)
}
async fn keep(
&self,
conn: &mut Conn,
i: usize,
round: i32,
source: &str,
) -> Result<u64, Error> {
let select = format!(
"SELECT x.replica_id, {i}, x.row_key, NULL FROM ({source}) AS x(replica_id, row_key)"
);
self.add(conn, Set::Kept(i, false), &select, round).await
}
async fn keep_for_older(
&self,
conn: &mut Conn,
i: usize,
round: i32,
source: &str,
) -> Result<u64, Error> {
let select = format!(
"SELECT x.replica_id, {i}, x.row_key, NULL FROM ({source}) AS x(replica_id, row_key)"
);
self.add(conn, Set::Kept(i, true), &select, round).await
}
async fn add(&self, conn: &mut Conn, set: Set, select: &str, round: i32) -> Result<u64, Error> {
let sql = match set {
Set::Suspect => format!(
"INSERT INTO sync_suspect (replica_id, rel, row_key, round) \
SELECT q.replica_id, q.rel, q.row_key, $1 FROM ({select}) AS q(replica_id, rel, row_key, why) \
ON CONFLICT DO NOTHING"
),
Set::Blocked => format!(
"INSERT INTO sync_blocked (replica_id, rel, row_key, why, round) \
SELECT q.replica_id, q.rel, q.row_key, q.why, $1 FROM ({select}) AS q(replica_id, rel, row_key, why) \
ON CONFLICT DO NOTHING"
),
Set::Kept(i, older) => {
let plan = &self.plans[i];
format!(
"INSERT INTO sync_kept (replica_id, rel, row_key, round, sends, up, down, older) \
SELECT DISTINCT ON (s.replica_id, s.rel, s.row_key) \
s.replica_id, s.rel, s.row_key, $1, s.sends, s.sends OR s.doubt, \
s.lacks OR s.doubt, {older} \
FROM (SELECT q.replica_id, q.rel, q.row_key, \
h.replica_id IS NULL OR h.refused AS lacks, \
(h.replica_id IS NULL OR h.refused OR {}) AND t.{} IS NOT NULL AS sends, \
d.replica_id IS NOT NULL AS doubt \
FROM ({select}) AS q(replica_id, rel, row_key, why) \
{} \
LEFT JOIN sync_changed c ON c.rel = q.rel AND c.row_key = q.row_key \
LEFT JOIN sync_suspect d ON d.replica_id = q.replica_id AND d.rel = q.rel \
AND d.row_key = q.row_key \
LEFT JOIN {} t ON {} \
WHERE NOT EXISTS (SELECT 1 FROM sync_blocked b WHERE b.replica_id = q.replica_id \
AND b.rel = q.rel AND b.row_key = q.row_key) {}) AS s \
WHERE s.lacks OR s.sends OR s.doubt \
ON CONFLICT (replica_id, rel, row_key) DO UPDATE SET older = false, round = $1 \
WHERE sync_kept.older AND NOT EXCLUDED.older",
stale("c.noted"),
ident(plan.key[0].name),
holds("LEFT JOIN", "h", "q.replica_id", &literal(plan.table), "q.row_key"),
plan.relation.sql(),
self.row_is(i, "q.row_key"),
self.wanted_sql(
i,
&format!("s.id = q.replica_id AND {}", self.row_is(i, "q.row_key"))
)
.map_or(String::new(), |whole| format!(
"AND NOT (q.replica_id IN (SELECT replica_id FROM sync_live WHERE fresh) \
AND EXISTS ({whole}))"
)),
)
}
};
Ok(sqlx::query(&sql)
.bind(round)
.execute(&mut *conn)
.await?
.rows_affected())
}
async fn conflicts(&self, conn: &mut Conn, asked: &Asked, round: i32) -> Result<bool, Error> {
let blocked = self.present(conn, "sync_blocked").await?;
let mut found = 0;
for (c, reference) in self
.references()
.filter(|(_, r)| blocked.contains(&r.parent))
{
let mut sources = vec![format!(
"SELECT k.replica_id, k.row_key FROM sync_kept k WHERE k.rel = {c}"
)];
sources.extend(
self.asked_sends(c, asked, "true")
.map(|sends| format!("SELECT a.replica_id, a.row_key FROM ({sends}) a")),
);
let select = format!(
"SELECT k.replica_id, {c}, k.row_key, \
format('it points at `%s` %s, which cannot go: %s', {}, b.row_key, b.why) \
FROM ({}) AS k(replica_id, row_key) JOIN {} t ON {} \
JOIN sync_blocked b ON b.replica_id = k.replica_id AND b.rel = {} \
AND b.row_key = {}",
literal(self.plans[reference.parent].table),
sources.join(" UNION ALL "),
self.plans[c].relation.sql(),
self.row_is(c, "k.row_key"),
reference.parent,
key_sql("t", &reference.columns),
);
found += self.add(conn, Set::Blocked, &select, round).await?;
}
Ok(found > 0)
}
async fn frontier(
&self,
conn: &mut Conn,
set: &str,
round: i32,
) -> Result<BTreeSet<usize>, Error> {
let rels: Vec<i32> =
sqlx::query_scalar(&format!("SELECT DISTINCT rel FROM {set} WHERE round = $1"))
.bind(round)
.fetch_all(&mut *conn)
.await?;
Ok(rels
.into_iter()
.filter_map(|r| usize::try_from(r).ok())
.collect())
}
async fn present(&self, conn: &mut Conn, set: &str) -> Result<BTreeSet<usize>, Error> {
let rels: Vec<i32> = sqlx::query_scalar(&format!("SELECT DISTINCT rel FROM {set}"))
.fetch_all(&mut *conn)
.await?;
Ok(rels
.into_iter()
.filter_map(|r| usize::try_from(r).ok())
.collect())
}
fn references(&self) -> impl Iterator<Item = (usize, &Reference)> {
self.plans
.iter()
.enumerate()
.flat_map(|(c, p)| p.references.iter().map(move |r| (c, r)))
}
fn followers(&self) -> impl Iterator<Item = (usize, &'static str, usize)> + '_ {
self.plans.iter().enumerate().filter_map(|(f, plan)| {
let Route::Follows(column) = plan.route else {
return None;
};
let reference = plan.references.iter().find(|r| r.columns == [column])?;
Some((f, column, reference.parent))
})
}
fn key_of_row(&self, i: usize) -> String {
key_sql(
"t",
&self.plans[i].key.iter().map(|c| c.name).collect::<Vec<_>>(),
)
}
fn row_is(&self, i: usize, text: &str) -> String {
let plan = &self.plans[i];
key_eq(
"t",
&plan.key.iter().map(|c| c.name).collect::<Vec<_>>(),
&plan.key,
text,
)
}
fn points_at(&self, reference: &Reference, text: &str) -> String {
key_eq(
"t",
&reference.columns,
&self.plans[reference.parent].key,
text,
)
}
fn key_among(&self, i: usize, keys: &str, by_replica: bool) -> String {
let plan = &self.plans[i];
let mut row: Vec<String> = plan
.key
.iter()
.map(|c| format!("t.{}", ident(c.name)))
.collect();
let mut typed: Vec<String> = plan
.key
.iter()
.enumerate()
.map(|(n, c)| {
let part = if plan.key.len() == 1 {
"k.row_key".to_owned()
} else {
format!("split_part(k.row_key, '|', {})", n + 1)
};
format!("({part})::{}", KeyPart::sql_type(c.key))
})
.collect();
let columns = if by_replica {
row.insert(0, "s.id".to_owned());
typed.insert(0, "k.replica_id".to_owned());
"replica_id, row_key"
} else {
"row_key"
};
format!(
"({}) IN (SELECT {} FROM ({keys}) AS k({columns}))",
row.join(", "),
typed.join(", ")
)
}
fn asked_sends(&self, i: usize, asked: &Asked, filter: &str) -> Option<String> {
let mut parts: Vec<String> = self.fresh_sends(i, asked, filter).into_iter().collect();
if [&asked.changed, &asked.regrouped, &asked.reassigned]
.iter()
.any(|asks| asks.contains(&i))
{
parts.push(format!(
"SELECT replica_id, row_key, lacks FROM sync_sends WHERE rel = {i}"
));
}
(!parts.is_empty()).then(|| parts.join(" UNION ALL "))
}
async fn keep_sends(&self, conn: &mut Conn, asked: &Asked) -> Result<(), Error> {
sqlx::Executor::execute(&mut *conn, "DELETE FROM sync_sends").await?;
let held = "IN (SELECT replica_id FROM sync_live WHERE NOT fresh)";
for i in 0..self.plans.len() {
let table = literal(self.plans[i].table);
let reassigned = format!("IN (SELECT replica_id FROM sync_reassigned WHERE rel = {i})");
let keys: Vec<String> = [
(&asked.changed, "sync_changed"),
(&asked.regrouped, "sync_regrouped"),
]
.into_iter()
.filter(|(asks, _)| asks.contains(&i))
.map(|(_, of)| format!("SELECT row_key FROM {of} WHERE rel = {i}"))
.collect();
let mut sets = Vec::new();
if !keys.is_empty() {
let keys = keys.join(" UNION ");
let mut those = format!("{held} AND {}", self.key_among(i, &keys, false));
let mut holders = format!("h.replica_id {held}");
if asked.reassigned.contains(&i) {
those = format!("{those} AND s.id NOT {reassigned}");
holders = format!("{holders} AND h.replica_id NOT {reassigned}");
}
sets.push((
self.wanted_sql(i, &format!("s.id {those}")),
format!(
"SELECT * FROM ({}) h WHERE {holders}",
held_by_key(&table, &keys)
),
));
}
if asked.reassigned.contains(&i) {
let replicas = format!(
"SELECT x.replica_id FROM sync_reassigned x JOIN sync_live l USING (replica_id) \
WHERE x.rel = {i} AND NOT l.fresh"
);
sets.push((
self.wanted_sql(i, &format!("s.id {held} AND s.id {reassigned}")),
held_by_replica(&table, &replicas),
));
}
for (wanted, holds) in sets {
let Some(wanted) = wanted else { continue };
sqlx::query(&format!(
"WITH w AS MATERIALIZED (SELECT w.replica_id, w.key FROM ({wanted}) w), \
h AS MATERIALIZED (SELECT h.replica_id, h.row_key, h.refused, {} AS stale \
FROM ({holds}) h \
LEFT JOIN sync_changed c ON c.rel = {i} AND c.row_key = h.row_key), \
doubted AS (INSERT INTO sync_suspect (replica_id, rel, row_key, round) \
SELECT q.replica_id, {i}, q.row_key, 0 \
FROM (SELECT replica_id, row_key FROM h EXCEPT TABLE w) q \
ON CONFLICT DO NOTHING) \
INSERT INTO sync_sends \
SELECT {i}, q.row_key, q.replica_id, q.lacks \
FROM (SELECT q.*, true AS lacks \
FROM (TABLE w EXCEPT SELECT replica_id, row_key FROM h WHERE NOT refused) q \
UNION ALL \
SELECT q.*, false \
FROM (SELECT replica_id, row_key FROM h WHERE NOT refused AND stale \
INTERSECT TABLE w) q) AS q(replica_id, row_key, lacks) \
WHERE NOT EXISTS (SELECT 1 FROM sync_blocked b WHERE b.replica_id = q.replica_id \
AND b.rel = {i} AND b.row_key = q.row_key)",
stale("c.noted")
))
.execute(&mut *conn)
.await?;
}
}
analyze(conn, "sync_sends").await
}
fn fresh_sends(&self, i: usize, asked: &Asked, filter: &str) -> Option<String> {
if !asked.fresh {
return None;
}
let wanted = self.wanted_sql(
i,
&format!("{filter} AND s.id IN (SELECT replica_id FROM sync_live WHERE fresh)"),
)?;
Some(format!(
"SELECT w.replica_id, w.key AS row_key, true AS lacks FROM ({wanted}) w \
WHERE NOT EXISTS (SELECT 1 FROM sync_blocked b WHERE b.replica_id = w.replica_id \
AND b.rel = {i} AND b.row_key = w.key)"
))
}
fn held_asked(&self, i: usize, asked: &Asked) -> Option<String> {
let table = literal(self.plans[i].table);
let mut parts = Vec::new();
for (asks, of) in [
(&asked.changed, "sync_changed"),
(&asked.regrouped, "sync_regrouped"),
] {
if asks.contains(&i) {
let keys = format!("SELECT row_key FROM {of} WHERE rel = {i}");
parts.push(held_by_key(&table, &keys));
}
}
if asked.reassigned.contains(&i) {
let replicas = format!("SELECT replica_id FROM sync_reassigned WHERE rel = {i}");
parts.push(held_by_replica(&table, &replicas));
}
(!parts.is_empty()).then(|| {
format!(
"SELECT h.replica_id, h.row_key FROM ({}) AS h \
WHERE h.replica_id IN (SELECT replica_id FROM sync_live WHERE NOT fresh)",
parts.join(" UNION ")
)
})
}
async fn asked(&self, conn: &mut Conn) -> Result<Asked, Error> {
let rows: Vec<(String, i32)> = sqlx::query_as(
"SELECT DISTINCT 'changed', rel FROM sync_changed \
UNION ALL SELECT DISTINCT 'regrouped', rel FROM sync_regrouped \
UNION ALL SELECT DISTINCT 'reassigned', rel FROM sync_reassigned \
UNION ALL SELECT 'fresh', 0 WHERE EXISTS (SELECT 1 FROM sync_live WHERE fresh)",
)
.fetch_all(&mut *conn)
.await?;
let mut asked = Asked::default();
for (what, rel) in rows {
let Ok(i) = usize::try_from(rel) else {
continue;
};
match what.as_str() {
"changed" => asked.changed.insert(i),
"regrouped" => asked.regrouped.insert(i),
"reassigned" => asked.reassigned.insert(i),
_ => {
asked.fresh = true;
true
}
};
}
Ok(asked)
}
async fn touch(&self, conn: &mut Conn) -> Result<(), Error> {
let mut sources = vec![
"SELECT replica_id FROM sync_live WHERE fresh".to_owned(),
"SELECT replica_id FROM sync_kept WHERE sends".to_owned(),
format!("SELECT replica_id FROM ({}) AS d", Self::DROPPED),
];
sources.push("SELECT DISTINCT replica_id FROM sync_sends".to_owned());
for source in sources {
sqlx::query(&format!(
"INSERT INTO sync_touched {source} ON CONFLICT DO NOTHING"
))
.execute(&mut *conn)
.await?;
}
Ok(())
}
const DROPPED: &'static str = "SELECT d.replica_id, d.rel, d.row_key FROM sync_suspect d \
JOIN sync_live USING (replica_id) \
WHERE NOT EXISTS (SELECT 1 FROM sync_kept k WHERE k.replica_id = d.replica_id \
AND k.rel = d.rel AND k.row_key = d.row_key)";
pub(crate) async fn dropping(
&self,
conn: &mut Conn,
) -> Result<BTreeMap<Uuid, BTreeSet<String>>, Error> {
let rows: Vec<(Uuid, i32)> = sqlx::query_as(&format!(
"SELECT DISTINCT d.replica_id, d.rel FROM ({}) AS d",
Self::DROPPED
))
.fetch_all(&mut *conn)
.await?;
let mut dropping: BTreeMap<Uuid, BTreeSet<String>> = BTreeMap::new();
for (replica, i) in rows {
let table = usize::try_from(i).map_or("", |i| self.table(i));
dropping
.entry(replica)
.or_default()
.insert(table.to_owned());
}
Ok(dropping)
}
pub(crate) async fn touched(&self, conn: &mut Conn) -> Result<BTreeSet<Uuid>, Error> {
Ok(sqlx::query_scalar("SELECT replica_id FROM sync_touched")
.fetch_all(&mut *conn)
.await?
.into_iter()
.collect())
}
pub(crate) async fn open_sent(&self, conn: &mut Conn) -> Result<(), Error> {
let asked = self.asked(conn).await?;
let mut parts = Vec::new();
for &i in &self.order {
let plan = &self.plans[i];
let key = self.key_of_row(i);
let unkept = |replica: &str, key: &str| {
format!(
"NOT EXISTS (SELECT 1 FROM sync_kept k WHERE k.replica_id = {replica} \
AND k.rel = {i} AND k.row_key = {key})"
)
};
let fresh = format!(
"s.id IN (SELECT replica_id FROM sync_live WHERE fresh) \
AND NOT EXISTS (SELECT 1 FROM sync_blocked b WHERE b.replica_id = s.id \
AND b.rel = {i} AND b.row_key = {key}) AND {}",
unkept("s.id", &key)
);
if asked.fresh && matches!(plan.route, Route::Broadcast) {
let provisioned = match self.replicas.partition {
Some(column) => format!(
"WHERE t.{} IN (SELECT s.partition FROM {} \
WHERE s.id IN (SELECT replica_id FROM sync_live WHERE fresh))",
ident(column),
self.replicas_sql()
),
None => String::new(),
};
parts.push(format!(
"(SELECT {i}, t.key, s.id FROM (SELECT {key} AS key, t.* FROM {} t {provisioned} \
ORDER BY 1 OFFSET 0) t \
CROSS JOIN LATERAL (SELECT s.id FROM {} WHERE {} AND {fresh} OFFSET 0) s)",
plan.active,
self.replicas_sql(),
self.same_partition(),
));
} else if let Some(wanted) = self.wanted_sql(i, &fresh).filter(|_| asked.fresh) {
let by = match plan.route {
Route::Grouped { .. } => "2".to_owned(),
_ => plan
.key
.iter()
.map(|c| format!("t.{}", ident(c.name)))
.collect::<Vec<_>>()
.join(", "),
};
parts.push(format!(
"(SELECT {i}, x.key, x.replica_id FROM ({wanted} ORDER BY {by}) AS x)"
));
}
parts.push(format!(
"(SELECT {i}, s.row_key, s.replica_id FROM sync_sends s \
WHERE s.rel = {i} AND {} ORDER BY s.row_key)",
unkept("s.replica_id", "s.row_key")
));
parts.push(format!(
"(SELECT k.rel, k.row_key, k.replica_id FROM sync_kept k \
WHERE k.rel = {i} AND k.sends ORDER BY k.row_key)"
));
}
sqlx::Executor::execute(
&mut *conn,
format!(
"DECLARE sync_sent NO SCROLL CURSOR FOR {}",
parts.join(" UNION ALL ")
)
.as_str(),
)
.await?;
Ok(())
}
async fn settled(&self, conn: &mut Conn, asked: &Asked) -> Result<(), Error> {
for &i in &asked.changed {
let table = literal(self.plans[i].table);
let quarantined =
format!("SELECT replica_id, row_key FROM sync.quarantine WHERE relation = {table}");
let mut sent = vec![format!(
"SELECT replica_id, row_key FROM sync_kept WHERE rel = {i} AND sends"
)];
sent.extend(
self.fresh_sends(i, asked, &self.key_among(i, &quarantined, true))
.map(|sends| format!("SELECT replica_id, row_key FROM ({sends}) AS a")),
);
sent.push(format!(
"SELECT replica_id, row_key FROM sync_sends WHERE rel = {i}"
));
sqlx::query(&format!(
"INSERT INTO sync_settled \
SELECT q.replica_id, q.relation, q.row_key FROM sync.quarantine q \
JOIN sync_changed c ON q.relation = c.relation AND q.row_key = c.row_key \
WHERE c.rel = {i} AND (q.replica_id, q.row_key) NOT IN ({}) \
AND NOT EXISTS (SELECT 1 FROM sync_blocked b WHERE b.replica_id = q.replica_id \
AND b.rel = c.rel AND b.row_key = c.row_key)",
sent.join(" UNION ALL ")
))
.execute(&mut *conn)
.await?;
}
Ok(())
}
pub(crate) async fn settle(&self, conn: &mut Conn) -> Result<(), Error> {
sqlx::query(
"DELETE FROM sync.quarantine q USING sync_settled s \
WHERE q.replica_id = s.replica_id AND q.relation = s.relation AND q.row_key = s.row_key",
)
.execute(&mut *conn)
.await?;
for (c, reference) in self.references() {
let child = literal(self.plans[c].table);
sqlx::query(&format!(
"INSERT INTO sync.notes (kind, relation, row_key) \
SELECT DISTINCT 'retry', q.relation, q.row_key FROM sync_changed x \
JOIN {} t ON {} \
JOIN sync.quarantine q ON q.relation = {child} AND q.row_key = {} \
AND q.cause = 'server' \
WHERE x.rel = {} \
AND NOT EXISTS (SELECT 1 FROM sync.notes n \
WHERE n.done_at IS NULL AND n.kind = 'retry' \
AND n.relation = q.relation AND n.row_key = q.row_key)",
self.plans[c].relation.sql(),
self.points_at(reference, "x.row_key"),
self.key_of_row(c),
reference.parent,
))
.execute(&mut *conn)
.await?;
}
Ok(())
}
pub(crate) async fn open_dropped(&self, conn: &mut Conn) -> Result<(), Error> {
sqlx::Executor::execute(
&mut *conn,
format!(
"DECLARE sync_dropped NO SCROLL CURSOR FOR \
SELECT d.rel, d.row_key, d.replica_id FROM ({}) AS d \
ORDER BY d.replica_id, array_position({}, d.rel), d.row_key",
Self::DROPPED,
self.order_array(true)
)
.as_str(),
)
.await?;
Ok(())
}
pub(crate) async fn next(
&self,
conn: &mut Conn,
cursor: &str,
limit: usize,
) -> Result<Vec<(usize, String, Uuid)>, Error> {
let rows: Vec<(i32, String, Uuid)> =
sqlx::query_as(&format!("FETCH FORWARD {limit} FROM {cursor}"))
.fetch_all(&mut *conn)
.await?;
if rows.is_empty() {
sqlx::Executor::execute(&mut *conn, format!("CLOSE {cursor}").as_str()).await?;
}
Ok(rows
.into_iter()
.map(|(i, k, r)| (usize::try_from(i).unwrap_or(usize::MAX), k, r))
.collect())
}
fn order_array(&self, children_first: bool) -> String {
let mut order: Vec<String> = self.order.iter().map(ToString::to_string).collect();
if children_first {
order.reverse();
}
format!("ARRAY[{}]::int4[]", order.join(", "))
}
fn groups_through(
&self,
table: &str,
pick: impl Fn(&Group) -> &Relation,
) -> Vec<(usize, Group)> {
self.plans
.iter()
.enumerate()
.filter_map(|(i, p)| {
let declared = match p.route {
Route::Grouped { members, assigned } => [members, assigned],
_ => return None,
};
p.group
.as_ref()
.filter(|g| {
let name = &pick(g).name;
name == table && declared.contains(&name.as_str())
})
.map(|g| (i, g.clone()))
})
.collect()
}
pub(crate) async fn fitting(
&self,
conn: &mut Conn,
i: usize,
keys: &[String],
budget: usize,
) -> Result<usize, Error> {
let plan = &self.plans[i];
let sizes: Vec<i64> = sqlx::query_scalar(&format!(
"SELECT ({})::int8 \
FROM unnest($1::text[]) WITH ORDINALITY AS k(row_key, n) \
LEFT JOIN {} t ON {} ORDER BY k.n",
plan.size,
plan.relation.sql(),
self.row_is(i, "k.row_key"),
))
.bind(keys)
.fetch_all(&mut *conn)
.await?;
let mut total = 0;
let fit = sizes
.iter()
.take_while(|size| {
total += usize::try_from(**size).unwrap_or(usize::MAX);
total <= budget
})
.count();
Ok(fit.max(1))
}
pub(crate) async fn encode<P: std::marker::Sync + 'static>(
&self,
conn: &mut Conn,
codecs: &[Codec<P>],
i: usize,
keys: &[String],
largest_row: usize,
) -> Result<Encoded, Error> {
let plan = &self.plans[i];
let codec = codecs
.iter()
.find(|c| c.table.name == plan.table)
.ok_or_else(|| {
Error::Misuse(format!("no codec for mirrored table `{}`", plan.table))
})?;
let parsed: Vec<Key> = keys
.iter()
.map(|k| parse_key(&plan.key, k))
.collect::<Result<_, _>>()?;
let loaded = codec.load(conn, &parsed).await?;
let mut out = Encoded::default();
for (key, bytes) in loaded.rows {
let key = key_text(&key);
if bytes.len() > largest_row {
let why = format!(
"a row of {} bytes, larger than any message may be ({largest_row})",
bytes.len()
);
out.failed.insert(key, why);
} else {
out.rows.insert(key, bytes);
}
}
for (key, e) in loaded.failed {
out.failed.insert(key_text(&key), e.to_string());
}
for key in keys {
if !out.rows.contains_key(key) {
out.failed
.entry(key.clone())
.or_insert_with(|| "the plan's snapshot has no such row".to_owned());
}
}
Ok(out)
}
pub(crate) async fn replan(
&self,
conn: &mut Conn,
mut delta: Delta,
blocked: BTreeMap<(Uuid, usize, String), String>,
) -> Result<Delta, Error> {
for (row, why) in blocked {
delta.blocked.entry(row).or_insert(why);
}
self.plan_around(conn, delta).await
}
pub(crate) async fn provision(
&self,
conn: &mut Conn,
mut delta: Delta,
replicas: &[Uuid],
) -> Result<Delta, Error> {
delta.notes.extend(replicas.iter().map(|r| Note {
id: 0,
change: Change::Resync { replica: *r },
}));
self.plan_around(conn, delta).await
}
pub(crate) fn unsent(&self, delta: &Delta) -> Vec<Unsent> {
delta
.blocked
.iter()
.map(|((replica, i, key), reason)| Unsent {
replica: *replica,
table: self.plans[*i].table,
key: key.clone(),
reason: reason.clone(),
})
.collect()
}
pub(crate) fn snapshot_tables(&self) -> Vec<&'static str> {
self.order.iter().map(|&i| self.plans[i].table).collect()
}
}
#[derive(sqlx::FromRow)]
struct Noted {
id: i64,
kind: String,
relation: Option<String>,
row_key: Option<String>,
replica_id: Option<Uuid>,
previous: Option<serde_json::Value>,
}
impl Noted {
fn change(self) -> Option<Change> {
Some(match self.kind.as_str() {
"resync" => Change::Resync {
replica: self.replica_id?,
},
"row" | "retry" => Change::Row {
retry: self.kind == "retry",
table: self.relation?,
key: self.row_key?,
},
"members" => Change::Members {
table: self.relation?,
member: self.previous?,
},
"assigned" => Change::Assigned {
table: self.relation?,
replica: self.replica_id?,
},
_ => return None,
})
}
}
pub(crate) const READABLE: &str =
"((kind IN ('row', 'retry') AND relation IS NOT NULL AND row_key IS NOT NULL) \
OR (kind = 'members' AND relation IS NOT NULL AND previous IS NOT NULL) \
OR (kind = 'assigned' AND relation IS NOT NULL AND replica_id IS NOT NULL) \
OR (kind = 'resync' AND replica_id IS NOT NULL))";
pub(crate) async fn pending(conn: &mut Conn, limit: i64) -> Result<Vec<Note>, Error> {
let isolation: String = sqlx::query_scalar("SELECT current_setting('transaction_isolation')")
.fetch_one(&mut *conn)
.await?;
if !matches!(isolation.as_str(), "repeatable read" | "serializable") {
return Err(Error::Misuse(format!(
"the notes are read in a REPEATABLE READ transaction, and this one is {isolation}: \
a plan read across snapshots can send a row without its parent"
)));
}
let rows: Vec<Noted> = sqlx::query_as(&format!(
"SELECT id, kind, relation, row_key, replica_id, previous \
FROM sync.notes WHERE done_at IS NULL AND parked_at IS NULL AND {READABLE} \
AND noted_at <= now() \
ORDER BY id LIMIT $1"
))
.bind(limit)
.fetch_all(&mut *conn)
.await?;
Ok(rows
.into_iter()
.filter_map(|noted| {
let id = noted.id;
noted.change().map(|change| Note { id, change })
})
.collect())
}
pub(crate) fn resync_sql(source: &str) -> String {
format!(
"INSERT INTO sync.notes (kind, replica_id, partition_id) \
SELECT DISTINCT ON (r.id) 'resync', r.id, r.partition FROM ({source}) AS r(id, partition) \
WHERE NOT EXISTS (SELECT 1 FROM sync.notes p WHERE p.kind = 'resync' \
AND p.replica_id = r.id AND p.done_at IS NULL \
AND p.noted_at <= now())"
)
}
pub async fn note_resync(
conn: &mut Conn,
replica: Uuid,
partition: Option<Uuid>,
) -> Result<(), Error> {
sqlx::query(&resync_sql("SELECT $1::uuid, $2::uuid"))
.bind(replica)
.bind(partition)
.execute(&mut *conn)
.await?;
Ok(())
}
pub(crate) async fn mark_done(conn: &mut Conn, ids: &[i64]) -> Result<(), Error> {
sqlx::query("UPDATE sync.notes SET done_at = now() WHERE id = ANY($1)")
.bind(ids)
.execute(&mut *conn)
.await?;
Ok(())
}
pub(crate) async fn prune(
conn: &mut Conn,
age: std::time::Duration,
limit: i64,
) -> Result<u64, Error> {
let done = sqlx::query(
"DELETE FROM sync.notes WHERE id IN (SELECT n.id FROM sync.notes n \
WHERE n.done_at < now() - make_interval(secs => $1) \
AND NOT EXISTS (SELECT 1 FROM sync.held h WHERE n.kind = 'row' \
AND h.relation = n.relation AND h.row_key = n.row_key \
AND h.noted < n.id) \
AND NOT EXISTS (SELECT 1 FROM sync.gone g WHERE n.kind = 'row' \
AND g.relation = n.relation AND g.row_key = n.row_key \
AND g.noted < n.id) \
ORDER BY n.done_at LIMIT $2)",
)
.bind(age.as_secs_f64())
.bind(limit)
.execute(&mut *conn)
.await?;
Ok(done.rows_affected())
}
fn group_of(
foreign_keys: &[ForeignKey],
plan: &Plan,
members: &str,
assigned: &str,
replicas: &str,
) -> Result<Group, Error> {
let bad = |why: &str| {
Error::Misuse(format!(
"`{}` is grouped by `{members}` and `{assigned}`, but {why}",
plan.table
))
};
fn one<'f>(
found: Vec<&'f ForeignKey>,
what: &str,
bad: impl Fn(&str) -> Error,
) -> Result<&'f ForeignKey, Error> {
match found.as_slice() {
[fk] => Ok(*fk),
[] => Err(bad(&format!("no {what}"))),
_ => Err(bad(&format!("more than one {what}"))),
}
}
let (member_table, _, member_columns, parent_columns) = one(
foreign_keys
.iter()
.filter(|(c, p, _, _)| c.name == members && *p == plan.relation)
.collect(),
"member key points at it",
bad,
)?;
let member_row: Option<Vec<String>> = plan
.key
.iter()
.map(|k| {
parent_columns
.iter()
.position(|c| c == k.name)
.map(|i| member_columns[i].clone())
})
.collect();
let member_row = member_row
.filter(|_| parent_columns.len() == plan.key.len())
.ok_or_else(|| bad("its members do not point at it by its key"))?;
let (assigned_table, _, replica_columns, _) = one(
foreign_keys
.iter()
.filter(|(c, p, _, _)| c.name == assigned && p.name == replicas)
.collect(),
"assigned key points at a replica",
bad,
)?;
let [assigned_replica] = replica_columns.as_slice() else {
return Err(bad(
"the assigned table points at a replica by more than its id",
));
};
let into_groups = |table: &Relation| -> Vec<&ForeignKey> {
foreign_keys
.iter()
.filter(|(c, p, _, _)| c == table && *p != plan.relation && p.name != replicas)
.collect()
};
let group = into_groups(member_table)
.into_iter()
.find_map(|(_, groups, members_cols, members_parent)| {
let (_, _, assigned_cols, assigned_parent) = into_groups(assigned_table)
.into_iter()
.find(|(_, p, _, _)| p == groups)?;
members_parent
.iter()
.zip(members_cols)
.map(|(parent, m)| {
assigned_parent
.iter()
.position(|a| a == parent)
.map(|i| (m.clone(), assigned_cols[i].clone()))
})
.collect::<Option<Vec<_>>>()
})
.ok_or_else(|| bad("the two do not point at one table of groups"))?;
Ok(Group {
members: member_table.clone(),
member_row,
group,
assigned: assigned_table.clone(),
assigned_replica: assigned_replica.clone(),
})
}
async fn typed_columns(conn: &mut Conn, relation: &str) -> Result<Vec<(String, String)>, Error> {
Ok(sqlx::query_as(
"SELECT a.attname::text, format_type(a.atttypid, NULL) FROM pg_attribute a \
WHERE a.attrelid = to_regclass($1) AND a.attnum > 0 AND NOT a.attisdropped",
)
.bind(relation)
.fetch_all(&mut *conn)
.await?)
}
async fn readable(conn: &mut Conn, sql: &str, what: &str) -> Result<(), Error> {
let mut tx = sqlx::Connection::begin(&mut *conn).await?;
let read = sqlx::query(sql).execute(&mut *tx).await;
tx.rollback().await?;
match read {
Err(sqlx::Error::Database(e)) => Err(Error::Misuse(format!(
"{what} does not read: {}",
e.message()
))),
other => other.map(|_| ()).map_err(Error::from),
}
}
fn topological(plans: &[Plan]) -> Result<Vec<usize>, Error> {
let mut order = Vec::with_capacity(plans.len());
let mut state = vec![0u8; plans.len()];
fn visit(
i: usize,
plans: &[Plan],
state: &mut [u8],
order: &mut Vec<usize>,
) -> Result<(), Error> {
match state[i] {
2 => return Ok(()),
1 => {
return Err(Error::Misuse(format!(
"mirrored tables point at each other in a cycle through `{}`",
plans[i].table
)))
}
_ => {}
}
state[i] = 1;
for reference in &plans[i].references {
visit(reference.parent, plans, state, order)?;
}
state[i] = 2;
order.push(i);
Ok(())
}
for i in 0..plans.len() {
visit(i, plans, &mut state, &mut order)?;
}
Ok(order)
}
#[cfg(test)]
#[path = "plan_tests.rs"]
mod plan_tests;
#[cfg(test)]
#[path = "plan_bench.rs"]
mod plan_bench;
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) async fn install(conn: &mut Conn) {
sqlx::raw_sql("DROP SCHEMA IF EXISTS sync CASCADE")
.execute(&mut *conn)
.await
.expect("drop the schema");
for step in 1..=crate::SERVER_SCHEMA_VERSION {
sqlx::raw_sql(&migration(step).expect("a step of this build"))
.execute(&mut *conn)
.await
.expect("the schema");
}
}
}