Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

How a row travels: codecs

A codec is a table’s row type, and how its rows are turned into bytes and back. The engine holds every table’s codec in one list, and never names a table itself.

You write the list, parents before children (examples/shop/rows/src/lib.rs):

/// A receipt is filed under the register that sent it, whatever it says.
fn place_receipt(receipt: &mut Receipt, register: Uuid, _: &()) {
    receipt.register_id = register;
}

/// Every table that travels, parents first.
pub fn codecs() -> Vec<Codec<()>> {
    vec![
        Codec::json::<Product>(None),
        Codec::json::<Receipt>(Some(place_receipt)),
        Codec::json::<Footer>(None),
    ]
}

JSON or protobuf

Codec::json sends a row as a JSON object of its columns, each value as its field’s serde writes it — a 64-bit integer as its decimal text, which a reader holding numbers as doubles would otherwise round: nothing else to declare, as long as serde writes each field under its own name. A field with #[serde(rename = "…")], a rename_all on the row, or a part brought in with #[sqlx(flatten)] that serde nests instead of flattening (give the field #[serde(flatten)] too) makes every row of the table fail to encode, the first time one travels. That is the simple way, and the one to start with.

Codec::new sends it as a protobuf message of your own, with a mapping each way — smaller on the wire, and a schema a replica in another language can generate code from:

pub fn product() -> Codec<()> {
    Codec::new::<Product, ProductRow>(product_to_wire, product_from_wire, None)
}

where ProductRow is a prost message:

/// A product on the wire.
#[derive(Clone, PartialEq, prost::Message)]
pub struct ProductRow {
    #[prost(string, tag = "1")]
    pub id: String,
    #[prost(string, tag = "2")]
    pub name: String,
    #[prost(int64, tag = "3")]
    pub price_cents: i64,
    #[prost(string, tag = "4")]
    pub updated_at: String,
}

and the two functions map the row to it and back, failing with a magic_sync::wire::MapError, which any error converts into with ?:

fn product_to_wire(p: Product) -> Result<ProductRow, MapError> {
    Ok(ProductRow {
        id: p.id.to_string(),
        name: p.name,
        price_cents: p.price_cents,
        updated_at: p.updated_at.to_rfc3339(),
    })
}

fn product_from_wire(m: ProductRow) -> Result<Product, MapError> {
    Ok(Product {
        id: Uuid::parse_str(&m.id)?,
        name: m.name,
        price_cents: m.price_cents,
        updated_at: DateTime::parse_from_rfc3339(&m.updated_at)?.with_timezone(&Utc),
    })
}

A row that does not encode is set aside on the server, for every replica that was to get it; one that does not decode on a replica is refused there, and reported. Either comes back once a fix lets it through.

What the server knows about a sender: P and place

Codec<P> carries a type P of yours: what the server knows about a replica that sends something up (here (): nothing). Every row a replica uploads goes through its codec’s place function with the replica’s id and that knowledge, so a receipt is filed under the register that sent it — the transport says which — whatever the row itself claims.

A multi-company shop would make P the register’s company, read when the upload arrives (Senders), and stamp it on every receipt.