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.