magic_sync 0.1.0

Keeps partial copies of PostgreSQL tables on SQLite replicas — offline-first, by comparing what each replica holds with what it should hold
Documentation
//! What the two backends spell differently, and nothing else.
//!
//! Generic code over [`Row`](crate::row::Row) never carries a backend `cfg`:
//! where postgres and sqlite differ, the difference is an item here.

use crate::{Conn, Error};

/// A name as SQL quotes it, whatever it is spelled with.
pub(crate) fn ident(name: &str) -> String {
    format!("\"{}\"", name.replace('"', "\"\""))
}

/// A string as an SQL literal.
pub(crate) fn literal(text: &str) -> String {
    format!("'{}'", text.replace('\'', "''"))
}

#[cfg(feature = "postgres")]
mod imp {
    use super::*;

    pub(crate) const NAME: &str = "postgres";

    /// Every version type compares natively here: a `timestamptz` to the
    /// microsecond.
    pub(crate) fn not_older(incoming: &str, stored: &str) -> String {
        format!("{incoming} >= {stored}")
    }

    /// Binds one statement may carry.
    pub(crate) const MAX_BINDS: usize = 65_535;

    /// `(column, nullable, generated)` for every column of `schema.table`.
    /// Read from `pg_attribute`, which answers whatever the role may see.
    pub(crate) async fn columns(
        conn: &mut Conn,
        schema: &str,
        table: &str,
    ) -> Result<Vec<(String, bool, bool)>, Error> {
        Ok(sqlx::query_as(
            "SELECT a.attname::text, NOT a.attnotnull, a.attgenerated <> '' \
               FROM pg_attribute a \
               JOIN pg_class c ON c.oid = a.attrelid \
               JOIN pg_namespace n ON n.oid = c.relnamespace \
              WHERE n.nspname = $1 AND c.relname = $2 AND a.attnum > 0 AND NOT a.attisdropped",
        )
        .bind(schema)
        .bind(table)
        .fetch_all(&mut *conn)
        .await?)
    }

    /// The primary key's columns of `schema.table`, in key order.
    pub(crate) async fn primary_key(
        conn: &mut Conn,
        schema: &str,
        table: &str,
    ) -> Result<Vec<String>, Error> {
        Ok(sqlx::query_scalar(
            "SELECT a.attname::text \
               FROM pg_index i \
               JOIN pg_class c ON c.oid = i.indrelid \
               JOIN pg_namespace n ON n.oid = c.relnamespace \
               JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = ANY (i.indkey) \
              WHERE i.indisprimary AND n.nspname = $1 AND c.relname = $2 \
              ORDER BY array_position(i.indkey::int2[], a.attnum)",
        )
        .bind(schema)
        .bind(table)
        .fetch_all(&mut *conn)
        .await?)
    }
}

#[cfg(feature = "sqlite")]
mod imp {
    use super::*;

    pub(crate) const NAME: &str = "sqlite";

    /// This instant, as the backend writes a timestamp.
    pub(crate) const NOW: &str = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";

    /// The foreign keys of `table`, as `(parent, its columns, the parent's
    /// columns)`: a key naming no parent columns names the parent's primary
    /// key.
    pub(crate) async fn pointing_from(
        conn: &mut Conn,
        table: &str,
    ) -> Result<Vec<(String, Vec<String>, Vec<String>)>, Error> {
        let rows: Vec<(i64, String, String, Option<String>)> = sqlx::query_as(
            r#"SELECT f.id, f."table", f."from", f."to"
                 FROM pragma_foreign_key_list(?1) f ORDER BY f.id, f.seq"#,
        )
        .bind(table)
        .fetch_all(&mut *conn)
        .await?;
        let mut out: Vec<(i64, String, Vec<String>, Vec<String>)> = Vec::new();
        for (id, parent, from, to) in rows {
            if out.last().is_none_or(|(last, ..)| *last != id) {
                out.push((id, parent, Vec::new(), Vec::new()));
            }
            let (.., from_columns, to_columns) = out.last_mut().expect("just pushed");
            from_columns.push(from);
            to_columns.extend(to);
        }
        let mut keys = Vec::with_capacity(out.len());
        for (_, parent, from, to) in out {
            if to.len() == from.len() {
                keys.push((parent, from, to));
                continue;
            }
            let primary: Vec<String> = sqlx::query_scalar(
                "SELECT name FROM pragma_table_info(?1) WHERE pk > 0 ORDER BY pk",
            )
            .bind(&parent)
            .fetch_all(&mut *conn)
            .await?;
            keys.push((parent, from, primary));
        }
        Ok(keys)
    }

    /// The foreign keys that point at rows of `table`, whatever they do on
    /// delete — refuse it (`NO ACTION`, `RESTRICT`, deferred or not), or
    /// change the row that points (`CASCADE`, `SET NULL`, `SET DEFAULT`),
    /// which would change a row of the replica's own the server never sent —
    /// as `(child, its columns, the parent's columns)` with the key's id in
    /// the child's list of them. A key naming no parent columns names the
    /// parent's primary key, `key`.
    pub(crate) async fn pointing(
        conn: &mut Conn,
        table: &str,
        key: &[&str],
    ) -> Result<Vec<Pointer>, Error> {
        let rows: Vec<(String, i64, String, Option<String>)> = sqlx::query_as(
            r#"SELECT m.name, f.id, f."from", f."to"
                 FROM sqlite_master m JOIN pragma_foreign_key_list(m.name) f
                WHERE m.type = 'table' AND f."table" = ?1 COLLATE NOCASE
                ORDER BY m.name, f.id, f.seq"#,
        )
        .bind(table)
        .fetch_all(&mut *conn)
        .await?;
        let mut out: Vec<Pointer> = Vec::new();
        for (child, id, from, to) in rows {
            if out.last().is_none_or(|p| p.child != child || p.id != id) {
                out.push(Pointer {
                    child,
                    id,
                    from: Vec::new(),
                    to: Vec::new(),
                });
            }
            let pointer = out.last_mut().expect("just pushed");
            pointer.from.push(from);
            pointer.to.extend(to);
        }
        for pointer in &mut out {
            if pointer.to.len() != pointer.from.len() {
                pointer.to = key.iter().map(|k| (*k).to_owned()).collect();
            }
        }
        Ok(out)
    }

    /// A foreign key into a table: the table it is of, its id there, its
    /// columns and the ones they point at.
    pub(crate) struct Pointer {
        pub(crate) child: String,
        pub(crate) id: i64,
        pub(crate) from: Vec<String>,
        pub(crate) to: Vec<String>,
    }

    /// `table.columns = +from.columns`, column by column: the comparison a
    /// foreign key makes — the parent column's collation, its affinity
    /// applied to the value pointing at it, which the unary `+` leaves with
    /// none of its own.
    pub(crate) fn points(table: &str, to: &[String], from: &str, columns: &[String]) -> String {
        to.iter()
            .zip(columns)
            .map(|(t, f)| format!("{table}.{} = +{from}.{}", ident(t), ident(f)))
            .collect::<Vec<_>>()
            .join(" AND ")
    }

    /// Whether `table` has rowids: one declared `WITHOUT ROWID` has none.
    pub(crate) async fn has_rowid(conn: &mut Conn, table: &str) -> Result<bool, Error> {
        let without: Option<i64> =
            sqlx::query_scalar("SELECT wr FROM pragma_table_list WHERE name = ?1")
                .bind(table)
                .fetch_optional(&mut *conn)
                .await?;
        Ok(without != Some(1))
    }

    /// The unique indexes of `table` other than its primary key, each with
    /// the SQL that made it — none for one a constraint declared.
    pub(crate) async fn unique_indexes(
        conn: &mut Conn,
        table: &str,
    ) -> Result<Vec<(String, Option<String>)>, Error> {
        Ok(sqlx::query_as(
            r#"SELECT i.name, m.sql FROM pragma_index_list(?1) i
                 LEFT JOIN sqlite_master m ON m.type = 'index' AND m.name = i.name
                WHERE i."unique" = 1 AND i.origin <> 'pk' ORDER BY i.name"#,
        )
        .bind(table)
        .fetch_all(&mut *conn)
        .await?)
    }

    /// Binds one statement may carry (`SQLITE_MAX_VARIABLE_NUMBER`).
    pub(crate) const MAX_BINDS: usize = 32_766;

    /// `(column, nullable, generated)` for every column of `table`. A primary
    /// key is NOT NULL only when declared so: sqlite lets a non-INTEGER one
    /// hold NULL.
    pub(crate) async fn columns(
        conn: &mut Conn,
        _schema: &str,
        table: &str,
    ) -> Result<Vec<(String, bool, bool)>, Error> {
        Ok(sqlx::query_as(
            r#"SELECT name, "notnull" = 0, hidden IN (2, 3) FROM pragma_table_xinfo(?) WHERE hidden <> 1"#,
        )
        .bind(table)
        .fetch_all(&mut *conn)
        .await?)
    }

    /// The primary key's columns of `table`, in key order.
    pub(crate) async fn primary_key(
        conn: &mut Conn,
        _schema: &str,
        table: &str,
    ) -> Result<Vec<String>, Error> {
        Ok(
            sqlx::query_scalar("SELECT name FROM pragma_table_info(?) WHERE pk > 0 ORDER BY pk")
                .bind(table)
                .fetch_all(&mut *conn)
                .await?,
        )
    }
}

pub(crate) use imp::*;

/// A table's name on this backend, as a `&'static str` a `const` can hold:
/// the derive's `QUALIFIED`, `"raw"."regions"` on postgres and `"regions"`
/// on sqlite, from the two names it quotes.
#[cfg(feature = "postgres")]
#[doc(hidden)]
#[macro_export]
macro_rules! __qualified {
    ($schema:literal, $table:literal) => {
        concat!($schema, ".", $table)
    };
}

#[cfg(feature = "sqlite")]
#[doc(hidden)]
#[macro_export]
macro_rules! __qualified {
    ($schema:literal, $table:literal) => {
        $table
    };
}