use crate::dialect::ident;
use crate::{Conn, Db, Error};
use sqlx::query_builder::Separated;
#[cfg(feature = "sqlite")]
use std::collections::HashSet;
use uuid::Uuid;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Mirror {
Catalog,
Record,
Both,
Setting,
}
impl Mirror {
pub(crate) fn goes_up(self) -> bool {
matches!(self, Mirror::Record | Mirror::Both | Mirror::Setting)
}
pub(crate) fn sent_down(self) -> bool {
matches!(self, Mirror::Catalog | Mirror::Both)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Apply {
Always,
IfUnchanged,
}
pub trait Setting: Row + Clone + Sync {
#[doc(hidden)]
const APPLY: Apply;
#[doc(hidden)]
fn take_desired(&mut self, from: Self);
#[doc(hidden)]
fn raise_version(&mut self);
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Route {
Referenced,
Broadcast,
Assigned(&'static str),
Grouped {
members: &'static str,
assigned: &'static str,
},
Follows(&'static str),
}
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct Column {
pub name: &'static str,
pub field: &'static str,
pub nullable: bool,
pub generated: bool,
pub replica_owned: bool,
pub desired: bool,
pub key: Option<KeyKind>,
pub version: Option<VersionKind>,
pub int64: bool,
pub compatible: fn(&<Db as sqlx::Database>::TypeInfo) -> bool,
}
pub trait Columns: crate::__private::Sealed {
#[doc(hidden)]
const VERSION: Option<&'static str>;
#[doc(hidden)]
const CREATED: Option<&'static str>;
#[doc(hidden)]
const REPLICA_OWNED: bool;
#[doc(hidden)]
fn columns(out: &mut Vec<Column>);
#[doc(hidden)]
fn push_binds<'args>(self, sep: &mut Separated<'_, 'args, Db, &'static str>);
}
pub trait Row:
Columns + for<'r> sqlx::FromRow<'r, <Db as sqlx::Database>::Row> + Send + Unpin + 'static
{
const TABLE: &'static str;
const SCHEMA: &'static str;
const KEY: &'static [&'static str];
const MIRROR: Mirror;
#[doc(hidden)]
const QUALIFIED: &'static str;
#[doc(hidden)]
const ROUTE: Route;
#[doc(hidden)]
const ACTIVE: Option<&'static str>;
#[doc(hidden)]
const RETIRED: Option<&'static str>;
#[doc(hidden)]
const UNMIRRORED: &'static [&'static str];
#[doc(hidden)]
const ORIGIN: Option<&'static str>;
fn key(&self) -> Key;
#[doc(hidden)]
fn version_text(&self) -> Option<String>;
fn key_text(&self) -> String {
key_text(&self.key())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum KeyPart {
Uuid(Uuid),
Int(i64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum KeyKind {
Uuid,
Integer,
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be a key column",
label = "a key column is a `Uuid`, an `i64`, an `i32` or an `i16`",
note = "a newtype around one of them implements `magic_sync::row::KeyValue` as its inner type does"
)]
pub trait KeyValue {
const KIND: KeyKind;
fn key_part(&self) -> KeyPart;
}
impl KeyValue for Uuid {
const KIND: KeyKind = KeyKind::Uuid;
fn key_part(&self) -> KeyPart {
KeyPart::Uuid(*self)
}
}
impl KeyValue for i64 {
const KIND: KeyKind = KeyKind::Integer;
fn key_part(&self) -> KeyPart {
KeyPart::Int(*self)
}
}
impl KeyValue for i32 {
const KIND: KeyKind = KeyKind::Integer;
fn key_part(&self) -> KeyPart {
KeyPart::Int((*self).into())
}
}
impl KeyValue for i16 {
const KIND: KeyKind = KeyKind::Integer;
fn key_part(&self) -> KeyPart {
KeyPart::Int((*self).into())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum VersionKind {
Integer,
Instant,
}
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot be a #[row(version)] column",
label = "a version is an integer or a `chrono` instant",
note = "a newtype around one of them implements `magic_sync::row::Version` as its inner type does"
)]
pub trait Version: Sized {
const KIND: VersionKind;
fn text(&self) -> String;
#[must_use]
fn raised(&self) -> Self;
}
impl Version for i16 {
const KIND: VersionKind = VersionKind::Integer;
fn text(&self) -> String {
self.to_string()
}
fn raised(&self) -> Self {
self.saturating_add(1)
}
}
impl Version for i32 {
const KIND: VersionKind = VersionKind::Integer;
fn text(&self) -> String {
self.to_string()
}
fn raised(&self) -> Self {
self.saturating_add(1)
}
}
impl Version for i64 {
const KIND: VersionKind = VersionKind::Integer;
fn text(&self) -> String {
self.to_string()
}
fn raised(&self) -> Self {
self.saturating_add(1)
}
}
fn raised_utc(at: chrono::DateTime<chrono::Utc>) -> chrono::DateTime<chrono::Utc> {
use chrono::SubsecRound;
let past = at.trunc_subsecs(6) + chrono::TimeDelta::microseconds(1);
chrono::Utc::now().trunc_subsecs(6).max(past)
}
fn instant_text(at: chrono::DateTime<chrono::Utc>) -> String {
use chrono::SubsecRound;
at.trunc_subsecs(6)
.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
}
impl<Tz: chrono::TimeZone> Version for chrono::DateTime<Tz> {
const KIND: VersionKind = VersionKind::Instant;
fn text(&self) -> String {
instant_text(self.with_timezone(&chrono::Utc))
}
fn raised(&self) -> Self {
raised_utc(self.with_timezone(&chrono::Utc)).with_timezone(&self.timezone())
}
}
impl Version for chrono::NaiveDateTime {
const KIND: VersionKind = VersionKind::Instant;
fn text(&self) -> String {
instant_text(self.and_utc())
}
fn raised(&self) -> Self {
raised_utc(self.and_utc()).naive_utc()
}
}
impl KeyPart {
pub(crate) fn parse(kind: Option<KeyKind>, text: &str) -> Option<Self> {
match kind {
Some(KeyKind::Integer) => text.parse().map(Self::Int).ok(),
_ => Uuid::parse_str(text).map(Self::Uuid).ok(),
}
}
#[cfg(feature = "postgres")]
pub(crate) fn sql_type(kind: Option<KeyKind>) -> &'static str {
match kind {
Some(KeyKind::Integer) => "bigint",
_ => "uuid",
}
}
fn bind<'q>(self, sep: &mut Separated<'_, 'q, Db, &'static str>) {
match self {
Self::Uuid(v) => sep.push_bind(v),
Self::Int(v) => sep.push_bind(v),
};
}
}
impl std::fmt::Display for KeyPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Uuid(v) => v.fmt(f),
Self::Int(v) => v.fmt(f),
}
}
}
pub type Key = Vec<KeyPart>;
pub(crate) fn key_text(key: &Key) -> String {
key.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("|")
}
fn declared<R: Columns>() -> Vec<Column> {
let mut out = Vec::new();
R::columns(&mut out);
out
}
fn written_names<R: Columns>() -> Vec<&'static str> {
declared::<R>()
.into_iter()
.filter(|c| !c.generated)
.map(|c| c.name)
.collect()
}
fn listed(names: &[&str]) -> String {
names
.iter()
.map(|n| ident(n))
.collect::<Vec<_>>()
.join(", ")
}
async fn write_keys<R>(exec: &mut Conn, rows: Vec<R>, conflict: &str) -> Result<Vec<Key>, Error>
where
R: Row,
{
if rows.is_empty() {
return Ok(Vec::new());
}
let mut seen = std::collections::HashSet::new();
let mut unique: Vec<R> = rows
.into_iter()
.rev()
.filter(|row| seen.insert(row.key()))
.collect();
unique.reverse();
let names = written_names::<R>();
let per_statement = (crate::dialect::MAX_BINDS / names.len().max(1)).max(1);
let kinds = key_kinds::<R>();
let mut written = Vec::new();
let mut rest = unique.into_iter().peekable();
while rest.peek().is_some() {
let chunk: Vec<R> = rest.by_ref().take(per_statement).collect();
let mut qb = sqlx::QueryBuilder::new(format!(
"INSERT INTO {} ({}) ",
R::QUALIFIED,
listed(&names)
));
qb.push_values(chunk, |mut sep, row| row.push_binds(&mut sep));
qb.push(conflict);
qb.push(format!(" RETURNING {}", listed(R::KEY)));
let rows = qb.build().fetch_all(&mut *exec).await?;
for row in &rows {
written.push(key_of_row(row, &kinds, 0)?);
}
}
Ok(written)
}
fn key_kinds<R: Row>() -> Vec<Option<KeyKind>> {
let columns = declared::<R>();
R::KEY
.iter()
.map(|k| columns.iter().find(|c| c.name == *k).and_then(|c| c.key))
.collect()
}
fn key_of_row(
row: &<Db as sqlx::Database>::Row,
kinds: &[Option<KeyKind>],
first: usize,
) -> Result<Key, Error> {
use sqlx::Row as _;
kinds
.iter()
.enumerate()
.map(|(i, kind)| {
Ok(match kind {
Some(KeyKind::Integer) => KeyPart::Int(row.try_get::<i64, _>(first + i)?),
_ => KeyPart::Uuid(row.try_get::<Uuid, _>(first + i)?),
})
})
.collect()
}
fn update_clause_where<R: Row>(take: impl Fn(&str) -> bool) -> String {
let set: Vec<String> = written_names::<R>()
.into_iter()
.filter(|c| !R::KEY.contains(c) && Some(*c) != R::CREATED && take(c))
.map(|c| format!("{0} = EXCLUDED.{0}", ident(c)))
.collect();
if set.is_empty() {
return format!(" ON CONFLICT ({}) DO NOTHING", listed(R::KEY));
}
format!(
" ON CONFLICT ({}) DO UPDATE SET {}",
listed(R::KEY),
set.join(", ")
)
}
fn key_filter<R: Row>(qb: &mut sqlx::QueryBuilder<'_, Db>, keys: &[Key]) {
qb.push(format!(" WHERE ({}) IN (", listed(R::KEY)));
for (i, key) in keys.iter().enumerate() {
if i > 0 {
qb.push(", ");
}
qb.push("(");
let mut sep = qb.separated(", ");
for part in key {
part.bind(&mut sep);
}
qb.push(")");
}
qb.push(")");
}
fn keys_per_statement<R: Row>() -> usize {
(crate::dialect::MAX_BINDS / R::KEY.len().max(1)).max(1)
}
pub(crate) async fn get_by_keys<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<Vec<R>, Error> {
let names: Vec<&str> = declared::<R>().into_iter().map(|c| c.name).collect();
let mut out = Vec::with_capacity(keys.len());
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(&names), R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
out.extend(qb.build_query_as::<R>().fetch_all(&mut *exec).await?);
}
Ok(out)
}
#[cfg(feature = "postgres")]
pub(crate) async fn delete_by_keys<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<u64, Error> {
let mut affected = 0;
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb = sqlx::QueryBuilder::new(format!("DELETE FROM {}", R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
affected += qb.build().execute(&mut *exec).await?.rows_affected();
}
Ok(affected)
}
#[cfg(feature = "sqlite")]
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct Retired {
pub(crate) deleted: u64,
pub(crate) kept: u64,
pub(crate) refused: Vec<(Key, String)>,
}
#[cfg(feature = "sqlite")]
pub(crate) async fn retire_by_keys<R: Row>(
exec: &mut Conn,
keys: &[Key],
held: &[Key],
server: &[(&str, Option<&str>)],
) -> Result<Retired, Error> {
let mut retired = Retired::default();
if keys.is_empty() && held.is_empty() {
return Ok(retired);
}
let staying = |child: &str| match server.iter().find(|(t, _)| t.eq_ignore_ascii_case(child)) {
None => Some(String::new()),
Some((_, Some(column))) => Some(format!(" AND c.{} IS NOT NULL", ident(column))),
Some((_, None)) => None,
};
keep::<R>(exec, held, true, &mut retired).await?;
let (mut pinned, pointed) = pointed_at::<R>(exec, keys, staying).await?;
let asked: HashSet<&Key> = keys.iter().collect();
pin(&mut pinned, &pointed, |(child, retired)| {
retired && !asked.contains(child)
});
let kept: Vec<Key> = keys
.iter()
.filter(|key| pinned.contains(*key))
.cloned()
.collect();
keep::<R>(exec, &kept, false, &mut retired).await?;
let going: Vec<Key> = keys
.iter()
.filter(|key| !pinned.contains(*key))
.cloned()
.collect();
delete_together::<R>(exec, going, "", &mut retired).await?;
Ok(retired)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn collect_by_keys<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<u64, Error> {
let (mut pinned, pointed) = pointed_at::<R>(exec, keys, |_| Some(String::new())).await?;
let asked: HashSet<&Key> = keys.iter().collect();
pin(&mut pinned, &pointed, |(child, _)| !asked.contains(child));
let going: Vec<Key> = keys
.iter()
.filter(|key| !pinned.contains(*key))
.cloned()
.collect();
let retired = R::RETIRED.map_or(" AND false".to_owned(), |column| {
format!(" AND {} IS NOT NULL", ident(column))
});
let mut gone = Retired::default();
delete_together::<R>(exec, going, &retired, &mut gone).await?;
Ok(gone.deleted)
}
#[cfg(feature = "sqlite")]
type Pointed = (Key, bool, Key);
#[cfg(feature = "sqlite")]
async fn pointed_at<R: Row>(
exec: &mut Conn,
keys: &[Key],
staying: impl Fn(&str) -> Option<String>,
) -> Result<(HashSet<Key>, Vec<Pointed>), Error> {
let mut pointers = Vec::new();
let mut inward = Vec::new();
for crate::dialect::Pointer {
child, from, to, ..
} in crate::dialect::pointing(exec, R::TABLE, R::KEY).await?
{
if child.eq_ignore_ascii_case(R::TABLE) {
inward.push((from, to));
continue;
}
let Some(stays) = staying(&child) else {
continue;
};
let on: Vec<String> = from
.iter()
.zip(&to)
.map(|(f, t)| format!("{}.{} = c.{}", R::QUALIFIED, ident(t), ident(f)))
.collect();
pointers.push(format!(
"EXISTS (SELECT 1 FROM {} c WHERE {}{stays})",
ident(&child),
on.join(" AND ")
));
}
let kinds = key_kinds::<R>();
let mut pinned = HashSet::new();
let mut pointed = Vec::new();
for chunk in keys.chunks(keys_per_statement::<R>()) {
if !pointers.is_empty() {
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(R::KEY), R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
qb.push(format!(" AND ({})", pointers.join(" OR ")));
for row in qb.build().fetch_all(&mut *exec).await? {
pinned.insert(key_of_row(&row, &kinds, 0)?);
}
}
for (from, to) in &inward {
let column = |alias: &str, names: &[&str]| {
names
.iter()
.map(|n| format!("{alias}.{}", ident(n)))
.collect::<Vec<_>>()
.join(", ")
};
let retired_column =
R::RETIRED.map_or("NULL".to_owned(), |r| format!("c.{}", ident(r)));
let on: Vec<String> = from
.iter()
.zip(to)
.map(|(f, t)| format!("p.{} = c.{}", ident(t), ident(f)))
.collect();
let mut qb = sqlx::QueryBuilder::new(format!(
"SELECT {}, {retired_column} IS NOT NULL, {} FROM {} c JOIN (SELECT * FROM {}",
column("c", R::KEY),
column("p", R::KEY),
R::QUALIFIED,
R::QUALIFIED
));
key_filter::<R>(&mut qb, chunk);
qb.push(format!(") p ON {}", on.join(" AND ")));
for row in qb.build().fetch_all(&mut *exec).await? {
use sqlx::Row as _;
let retired_child: bool = row.try_get(R::KEY.len())?;
pointed.push((
key_of_row(&row, &kinds, 0)?,
retired_child,
key_of_row(&row, &kinds, R::KEY.len() + 1)?,
));
}
}
}
Ok((pinned, pointed))
}
#[cfg(feature = "sqlite")]
fn pin(pinned: &mut HashSet<Key>, pointed: &[Pointed], stays: impl Fn((&Key, bool)) -> bool) {
loop {
let more: Vec<Key> = pointed
.iter()
.filter(|(child, retired, parent)| {
!pinned.contains(parent) && (pinned.contains(child) || stays((child, *retired)))
})
.map(|(_, _, parent)| parent.clone())
.collect();
if more.is_empty() {
return;
}
pinned.extend(more);
}
}
#[cfg(feature = "sqlite")]
async fn delete_together<R: Row>(
exec: &mut Conn,
mut going: Vec<Key>,
only: &str,
retired: &mut Retired,
) -> Result<(), Error> {
loop {
let before = retired.deleted;
let mut refused = Vec::new();
for chunk in going.chunks(keys_per_statement::<R>()) {
let delete = |keys: &[Key]| {
let mut qb = sqlx::QueryBuilder::new(format!("DELETE FROM {}", R::QUALIFIED));
key_filter::<R>(&mut qb, keys);
qb.push(only);
qb
};
refused.extend(one_by_one(exec, chunk, delete, &mut retired.deleted).await?);
}
if refused.is_empty() || retired.deleted == before {
retired.refused.extend(refused);
return Ok(());
}
going = refused.into_iter().map(|(key, _)| key).collect();
}
}
#[cfg(feature = "sqlite")]
async fn keep<R: Row>(
exec: &mut Conn,
keys: &[Key],
unfiled: bool,
retired: &mut Retired,
) -> Result<u64, Error> {
let Some(column) = R::RETIRED else {
let why = if unfiled {
format!(
"what this replica wrote of it has not reached the server, and `{}` has no \
retired column to keep it by",
R::TABLE
)
} else {
format!(
"a row here points at it, and `{}` has no retired column to keep it by",
R::TABLE
)
};
let kept = key_select::<R>(exec, keys).await?;
retired
.refused
.extend(kept.into_iter().map(|key| (key, why.clone())));
return Ok(0);
};
let retire = |keys: &[Key]| {
let mut qb = sqlx::QueryBuilder::new(format!(
"UPDATE {} SET {} = {}",
R::QUALIFIED,
ident(column),
crate::dialect::NOW
));
key_filter::<R>(&mut qb, keys);
qb.push(format!(" AND {} IS NULL", ident(column)));
qb
};
let before = retired.kept;
for chunk in keys.chunks(keys_per_statement::<R>()) {
let refused = one_by_one(exec, chunk, retire, &mut retired.kept).await?;
retired.refused.extend(refused);
}
Ok(retired.kept - before)
}
#[cfg(feature = "sqlite")]
pub(crate) struct Pointing {
temp: String,
child: String,
id: i64,
from: Vec<String>,
}
#[cfg(feature = "sqlite")]
pub(crate) async fn pointing<R: Row>(exec: &mut Conn, name: usize) -> Result<Vec<Pointing>, Error> {
let enforced: i64 = sqlx::query_scalar("PRAGMA foreign_keys")
.fetch_one(&mut *exec)
.await?;
if enforced == 0 {
return Ok(Vec::new());
}
let mut out = Vec::new();
for pointer in crate::dialect::pointing(exec, R::TABLE, R::KEY).await? {
if !crate::dialect::has_rowid(exec, &pointer.child).await? {
continue;
}
let temp = format!("sync_pointing_{}", name + out.len());
let from: Vec<String> = pointer
.from
.iter()
.enumerate()
.map(|(n, f)| format!("c.{} AS sync_from_{n}", ident(f)))
.collect();
let keys: Vec<String> = R::KEY
.iter()
.enumerate()
.map(|(n, k)| format!("p.{} AS sync_key_{n}", ident(k)))
.collect();
sqlx::query(&format!(
"CREATE TEMP TABLE {temp} AS SELECT c.rowid AS sync_child, {}, {} FROM {} c \
JOIN {} p ON {}",
from.join(", "),
keys.join(", "),
ident(&pointer.child),
R::QUALIFIED,
crate::dialect::points("p", &pointer.to, "c", &pointer.from)
))
.execute(&mut *exec)
.await?;
out.push(Pointing {
temp,
child: pointer.child,
id: pointer.id,
from: pointer.from,
});
}
Ok(out)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn broken<R: Row>(
exec: &mut Conn,
pointing: &[Pointing],
) -> Result<Vec<Key>, Error> {
let kinds = key_kinds::<R>();
let keys: Vec<String> = (0..R::KEY.len())
.map(|n| format!("w.sync_key_{n}"))
.collect();
let mut out = Vec::new();
for p in pointing {
let same: Vec<String> = p
.from
.iter()
.enumerate()
.map(|(n, f)| format!("w.sync_from_{n} IS c.{}", ident(f)))
.collect();
let rows = sqlx::query(&format!(
"SELECT DISTINCT {} FROM {} w JOIN {} c ON c.rowid = w.sync_child \
WHERE {} AND w.sync_child IN \
(SELECT rowid FROM pragma_foreign_key_check($1) WHERE fkid = $2)",
keys.join(", "),
p.temp,
ident(&p.child),
same.join(" AND ")
))
.bind(&p.child)
.bind(p.id)
.fetch_all(&mut *exec)
.await?;
for row in &rows {
out.push(key_of_row(row, &kinds, 0)?);
}
}
Ok(out)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn forget(exec: &mut Conn, pointing: &[Pointing]) -> Result<(), Error> {
for p in pointing {
sqlx::query(&format!("DROP TABLE temp.{}", p.temp))
.execute(&mut *exec)
.await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn orphaned<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<Vec<Key>, Error> {
if !crate::dialect::has_rowid(exec, R::TABLE).await? {
return Ok(Vec::new());
}
let kinds = key_kinds::<R>();
let mut out = Vec::new();
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(R::KEY), R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
qb.push(format!(
" AND rowid IN (SELECT rowid FROM pragma_foreign_key_check({}))",
crate::dialect::literal(R::TABLE)
));
for row in qb.build().fetch_all(&mut *exec).await? {
out.push(key_of_row(&row, &kinds, 0)?);
}
}
Ok(out)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn unparented<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<Vec<Key>, Error> {
let enforced: i64 = sqlx::query_scalar("PRAGMA foreign_keys")
.fetch_one(&mut *exec)
.await?;
if enforced == 0 {
return Ok(Vec::new());
}
let missing: Vec<String> = crate::dialect::pointing_from(exec, R::TABLE)
.await?
.into_iter()
.map(|(parent, from, to)| {
let present: Vec<String> = from
.iter()
.map(|f| format!("{}.{} IS NOT NULL", R::QUALIFIED, ident(f)))
.collect();
format!(
"({} AND NOT EXISTS (SELECT 1 FROM {} p WHERE {}))",
present.join(" AND "),
ident(&parent),
crate::dialect::points("p", &to, R::QUALIFIED, &from)
)
})
.collect();
if missing.is_empty() {
return Ok(Vec::new());
}
let kinds = key_kinds::<R>();
let mut orphans = Vec::new();
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(R::KEY), R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
qb.push(format!(" AND ({})", missing.join(" OR ")));
for row in qb.build().fetch_all(&mut *exec).await? {
orphans.push(key_of_row(&row, &kinds, 0)?);
}
}
Ok(orphans)
}
#[cfg(feature = "sqlite")]
async fn one_by_one<'k>(
exec: &mut Conn,
keys: &'k [Key],
build: impl Fn(&'k [Key]) -> sqlx::QueryBuilder<'k, Db>,
affected: &mut u64,
) -> Result<Vec<(Key, String)>, Error> {
if keys.is_empty() {
return Ok(Vec::new());
}
savepoint(exec).await?;
let all = build(keys).build().execute(&mut *exec).await;
match released(exec, all.map_err(Error::from)).await? {
Ok(done) => {
*affected += done.rows_affected();
return Ok(Vec::new());
}
Err(e) => tracing::warn!("rows refused together, tried one by one: {e}"),
}
let mut refused = Vec::new();
for key in keys {
savepoint(exec).await?;
let one = build(std::slice::from_ref(key))
.build()
.execute(&mut *exec)
.await;
match released(exec, one.map_err(Error::from)).await? {
Ok(done) => *affected += done.rows_affected(),
Err(e) => refused.push((key.clone(), e.to_string())),
}
}
Ok(refused)
}
#[cfg(feature = "sqlite")]
pub(crate) async fn key_select<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<Vec<Key>, Error> {
let kinds = key_kinds::<R>();
let mut out = Vec::new();
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(R::KEY), R::QUALIFIED));
key_filter::<R>(&mut qb, chunk);
for row in qb.build().fetch_all(&mut *exec).await? {
out.push(key_of_row(&row, &kinds, 0)?);
}
}
Ok(out)
}
pub(crate) async fn savepoint(exec: &mut Conn) -> Result<(), Error> {
sqlx::query("SAVEPOINT sync_rows")
.execute(&mut *exec)
.await?;
Ok(())
}
pub(crate) async fn released<T>(
exec: &mut Conn,
result: Result<T, Error>,
) -> Result<Result<T, Error>, Error> {
let refusal = match result {
Ok(value) => {
sqlx::query("RELEASE SAVEPOINT sync_rows")
.execute(&mut *exec)
.await?;
return Ok(Ok(value));
}
Err(e) if !e.is_refusal() => return Err(e),
Err(e) => e,
};
let rolled_back = sqlx::query("ROLLBACK TO SAVEPOINT sync_rows")
.execute(&mut *exec)
.await;
if rolled_back.is_err() {
return Err(refusal);
}
sqlx::query("RELEASE SAVEPOINT sync_rows")
.execute(&mut *exec)
.await?;
Ok(Err(refusal))
}
#[cfg(feature = "sqlite")]
pub(crate) async fn unretire_by_keys<R: Row>(exec: &mut Conn, keys: &[Key]) -> Result<(), Error> {
let Some(retired) = R::RETIRED else {
return Ok(());
};
for chunk in keys.chunks(keys_per_statement::<R>()) {
let mut qb = sqlx::QueryBuilder::new(format!(
"UPDATE {} SET {} = NULL",
R::QUALIFIED,
ident(retired)
));
key_filter::<R>(&mut qb, chunk);
qb.push(format!(" AND {} IS NOT NULL", ident(retired)));
qb.build().execute(&mut *exec).await?;
}
Ok(())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn live_keys<R: Row>(exec: &mut Conn) -> Result<Vec<Key>, Error> {
let live = R::RETIRED
.map(|retired| format!(" WHERE {} IS NULL", ident(retired)))
.unwrap_or_default();
keys_where::<R>(exec, &live).await
}
#[cfg(feature = "sqlite")]
pub(crate) async fn keys<R: Row>(exec: &mut Conn) -> Result<Vec<Key>, Error> {
keys_where::<R>(exec, "").await
}
#[cfg(feature = "sqlite")]
pub(crate) async fn retired_keys<R: Row>(exec: &mut Conn) -> Result<Vec<Key>, Error> {
let Some(retired) = R::RETIRED else {
return Ok(Vec::new());
};
keys_where::<R>(exec, &format!(" WHERE {} IS NOT NULL", ident(retired))).await
}
#[cfg(feature = "sqlite")]
async fn keys_where<R: Row>(exec: &mut Conn, filter: &str) -> Result<Vec<Key>, Error> {
let kinds = key_kinds::<R>();
let rows = sqlx::query(&format!(
"SELECT {} FROM {}{filter}",
listed(R::KEY),
R::QUALIFIED
))
.fetch_all(&mut *exec)
.await?;
Ok(rows
.iter()
.filter_map(|row| {
key_of_row(row, &kinds, 0)
.inspect_err(|e| {
tracing::error!(
table = R::TABLE,
"a key stored in a form the engine does not read — left out: {e}"
);
})
.ok()
})
.collect())
}
#[cfg(feature = "sqlite")]
pub(crate) async fn overwrite_except<R: Row>(
exec: &mut Conn,
rows: Vec<R>,
keep: &[&str],
) -> Result<Vec<Key>, Error> {
let conflict = update_clause_where::<R>(|c| !keep.contains(&c));
write_keys(exec, rows, &conflict).await
}
#[cfg(feature = "postgres")]
pub(crate) async fn update_held<R: Row>(
exec: &mut Conn,
rows: Vec<R>,
columns: &[&str],
sender: Uuid,
) -> Result<Vec<Key>, Error> {
let names = written_names::<R>();
let set: Vec<String> = names
.iter()
.filter(|c| columns.contains(c) && !R::KEY.contains(c))
.map(|c| format!("{0} = v.{0}", ident(c)))
.collect();
if rows.is_empty() || set.is_empty() {
return Ok(Vec::new());
}
let mut seen = std::collections::HashSet::new();
let mut unique: Vec<R> = rows
.into_iter()
.rev()
.filter(|row| seen.insert(row.key()))
.collect();
unique.reverse();
let matched: Vec<String> = R::KEY
.iter()
.map(|k| format!("t.{0} = v.{0}", ident(k)))
.collect();
let key_text: Vec<String> = R::KEY
.iter()
.map(|k| format!("t.{}::text", ident(k)))
.collect();
let returned: Vec<String> = R::KEY.iter().map(|k| format!("t.{}", ident(k))).collect();
let per_statement = ((crate::dialect::MAX_BINDS - 1) / names.len().max(1)).max(1);
let kinds = key_kinds::<R>();
let mut written = Vec::new();
let mut rest = unique.into_iter().peekable();
while rest.peek().is_some() {
let chunk: Vec<R> = rest.by_ref().take(per_statement).collect();
let mut qb = sqlx::QueryBuilder::new(format!(
"UPDATE {} AS t SET {} FROM (",
R::QUALIFIED,
set.join(", ")
));
qb.push_values(chunk, |mut sep, row| row.push_binds(&mut sep));
qb.push(format!(
") AS v ({}) WHERE {} AND EXISTS (SELECT 1 FROM sync.held h WHERE h.replica_id = ",
listed(&names),
matched.join(" AND ")
));
qb.push_bind(sender);
qb.push(format!(
" AND h.relation = {} AND h.row_key = concat_ws('|', {})) RETURNING {}",
crate::dialect::literal(R::TABLE),
key_text.join(", "),
returned.join(", ")
));
for row in &qb.build().fetch_all(&mut *exec).await? {
written.push(key_of_row(row, &kinds, 0)?);
}
}
Ok(written)
}
#[cfg(feature = "postgres")]
pub(crate) async fn upsert<R: Row>(exec: &mut Conn, rows: Vec<R>) -> Result<Vec<Key>, Error> {
write_keys(exec, rows, &newest::<R>(update_clause_where::<R>(|_| true))).await
}
#[cfg(feature = "postgres")]
pub(crate) async fn as_json<R: Row>(exec: &mut Conn, row: R) -> Result<serde_json::Value, Error> {
let names = written_names::<R>();
let mut qb = sqlx::QueryBuilder::new("SELECT to_jsonb(v) FROM (VALUES (");
let mut sep = qb.separated(", ");
row.push_binds(&mut sep);
qb.push(format!(")) AS v({})", listed(&names)));
Ok(qb.build_query_scalar().fetch_one(&mut *exec).await?)
}
#[cfg(feature = "postgres")]
pub(crate) async fn wanted<R: Row>(
exec: &mut Conn,
key: &str,
whole: &serde_json::Value,
) -> Result<R, Error> {
let table = Table::of::<R>();
let key_columns: Vec<Column> = table
.key
.iter()
.filter_map(|k| table.columns.iter().find(|c| c.name == *k).copied())
.collect();
let key = crate::plan::parse_key(&key_columns, key)?;
let laid: serde_json::Map<String, serde_json::Value> = whole
.as_object()
.into_iter()
.flatten()
.filter(|(name, _)| {
table.columns.iter().any(|c| {
c.name == name.as_str() && (c.desired || c.key.is_some() || c.version.is_some())
})
})
.map(|(name, value)| (name.clone(), value.clone()))
.collect();
let names: Vec<&str> = declared::<R>().into_iter().map(|c| c.name).collect();
let mut qb = sqlx::QueryBuilder::new(format!(
"SELECT {} FROM (SELECT (jsonb_populate_record(coalesce(\
(SELECT t FROM {} t",
listed(&names),
R::QUALIFIED
));
key_filter::<R>(&mut qb, std::slice::from_ref(&key));
qb.push(format!("), jsonb_populate_record(NULL::{}, ", R::QUALIFIED));
qb.push_bind(whole.clone());
qb.push(")), ");
qb.push_bind(serde_json::Value::Object(laid));
qb.push(")).*) w");
Ok(qb.build_query_as::<R>().fetch_one(&mut *exec).await?)
}
pub(crate) async fn versions<R: Row>(
exec: &mut Conn,
of: Option<Uuid>,
) -> Result<Vec<String>, Error> {
let names: Vec<&str> = declared::<R>().into_iter().map(|c| c.name).collect();
let mut qb =
sqlx::QueryBuilder::new(format!("SELECT {} FROM {}", listed(&names), R::QUALIFIED));
if let (Some(replica), Some(origin)) = (of, R::ORIGIN) {
qb.push(format!(" WHERE {} = ", ident(origin)));
qb.push_bind(replica);
}
Ok(qb
.build_query_as::<R>()
.fetch_all(&mut *exec)
.await?
.iter()
.map(|row| {
format!(
"{}\t{}",
row.key_text(),
row.version_text().unwrap_or_default()
)
})
.collect())
}
#[cfg(feature = "postgres")]
pub(crate) async fn overwrite<R: Row>(exec: &mut Conn, rows: Vec<R>) -> Result<Vec<Key>, Error> {
write_keys(exec, rows, &update_clause_where::<R>(|_| true)).await
}
#[cfg(feature = "postgres")]
pub(crate) async fn insert_ignore<R: Row>(
exec: &mut Conn,
rows: Vec<R>,
) -> Result<Vec<Key>, Error> {
let conflict = format!(" ON CONFLICT ({}) DO NOTHING", listed(R::KEY));
write_keys(exec, rows, &conflict).await
}
#[cfg(feature = "postgres")]
fn newest<R: Row>(mut conflict: String) -> String {
let Some(version) = R::VERSION.filter(|_| conflict.contains("DO UPDATE")) else {
return conflict;
};
conflict.push_str(" WHERE ");
conflict.push_str(&crate::dialect::not_older(
&format!("EXCLUDED.{}", ident(version)),
&format!("{}.{}", R::QUALIFIED, ident(version)),
));
conflict
}
pub(crate) struct Table {
pub(crate) name: &'static str,
pub(crate) schema: &'static str,
pub(crate) qualified: &'static str,
pub(crate) key: &'static [&'static str],
pub(crate) mirror: Mirror,
#[cfg(feature = "postgres")]
pub(crate) route: Route,
#[cfg(feature = "postgres")]
pub(crate) active: Option<&'static str>,
pub(crate) columns: Vec<Column>,
pub(crate) retired: Option<&'static str>,
pub(crate) unmirrored: &'static [&'static str],
pub(crate) origin: Option<&'static str>,
#[cfg(feature = "postgres")]
pub(crate) encoding: &'static str,
}
impl Table {
pub(crate) fn of<R: Row>() -> Self {
Self {
name: R::TABLE,
schema: R::SCHEMA,
qualified: R::QUALIFIED,
key: R::KEY,
mirror: R::MIRROR,
#[cfg(feature = "postgres")]
route: R::ROUTE,
#[cfg(feature = "postgres")]
active: R::ACTIVE,
columns: declared::<R>(),
retired: R::RETIRED,
unmirrored: R::UNMIRRORED,
origin: R::ORIGIN,
#[cfg(feature = "postgres")]
encoding: "",
}
}
pub(crate) fn replica_owned(&self) -> Vec<&'static str> {
self.columns
.iter()
.filter(|c| c.replica_owned)
.map(|c| c.name)
.collect()
}
fn contradictions(&self) -> Vec<String> {
let mut out = Vec::new();
for (i, column) in self.columns.iter().enumerate() {
if self.columns[..i].iter().any(|c| c.name == column.name) {
out.push(format!(
"the column `{}` is a field of the row twice, once in a part flattened in",
column.name
));
}
}
for name in self.unmirrored {
if self.columns.iter().any(|c| c.name == *name) {
out.push(format!(
"`{name}` is a field of the row, in a part flattened in, and is also `unmirrored`"
));
}
}
out
}
pub(crate) async fn differences(&self, conn: &mut Conn) -> Result<Vec<String>, Error> {
let mut out = self.contradictions();
let actual = crate::dialect::columns(conn, self.schema, self.name).await?;
if actual.is_empty() {
out.push(format!("the table is not on {}", crate::dialect::NAME));
return Ok(out);
}
let left_out = |name: &str| Some(name) == self.retired || self.unmirrored.contains(&name);
for (name, nullable, generated) in &actual {
if left_out(name) {
continue;
}
let Some(declared) = self.columns.iter().find(|c| c.name == name) else {
out.push(format!("the column `{name}` is not a field of the row"));
continue;
};
if declared.nullable != *nullable {
out.push(format!(
"`{name}` is {} in the table and {} in the row",
if *nullable { "nullable" } else { "NOT NULL" },
if declared.nullable {
"an Option"
} else {
"no Option"
},
));
}
if declared.generated != *generated {
out.push(format!(
"`{name}` is {}generated in the table and {}#[row(generated)] in the row",
if *generated { "" } else { "not " },
if declared.generated { "" } else { "not " },
));
}
}
for column in &self.columns {
if !actual.iter().any(|(name, _, _)| name == column.name) {
out.push(format!(
"the field `{}` is no column of the table",
column.name
));
}
}
let mut primary_key = crate::dialect::primary_key(conn, self.schema, self.name).await?;
let mut key: Vec<&str> = self.key.to_vec();
primary_key.sort_unstable();
key.sort_unstable();
if primary_key != key {
out.push(format!(
"the primary key is {primary_key:?}, and the row's key {key:?}"
));
}
let described =
sqlx::Executor::describe(&mut *conn, &format!("SELECT * FROM {}", self.qualified))
.await?;
for column in described.columns() {
let name = sqlx::Column::name(column);
let Some(declared) = self.columns.iter().find(|c| c.name == name) else {
continue;
};
let info = sqlx::Column::type_info(column);
if !(declared.compatible)(info) {
out.push(format!(
"`{name}` is {info} in the table, which its field does not decode"
));
}
}
Ok(out)
}
}
pub(crate) async fn parity(conn: &mut Conn, tables: &[&Table]) -> Result<(), Error> {
let mut wrong = Vec::new();
for table in tables {
for difference in table.differences(conn).await? {
wrong.push(format!("`{}.{}`: {difference}", table.schema, table.name));
}
}
if wrong.is_empty() {
return Ok(());
}
Err(Error::Misuse(format!(
"tables that are not their rows — change the struct or the table:\n{}",
wrong.join("\n")
)))
}
#[cfg(test)]
mod tests {
use super::*;
type Cents = i64;
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Row)]
#[row(
table = "order",
schema = "Shop",
mirror = "catalog",
key = "group, line",
active = "Shop.live orders",
retired = "retired at"
)]
struct Order {
group: Uuid,
line: i32,
r#type: String,
#[sqlx(rename = "Total Due")]
total: Cents,
note: Option<Cents>,
at: chrono::DateTime<chrono::Utc>,
}
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Columns)]
struct Label {
label: String,
}
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Row)]
#[row(table = "reports", mirror = "record")]
struct Report {
id: Uuid,
#[row(version)]
at: chrono::DateTime<chrono::Utc>,
}
#[test]
fn a_rows_key_text_joins_its_parts_in_their_one_form() {
let group = Uuid::parse_str("0192F6A0-0000-7000-8000-00000000000A").unwrap();
let order = Order {
group,
line: -3,
r#type: String::new(),
total: 0,
note: None,
at: chrono::Utc::now(),
};
assert_eq!(order.key_text(), "0192f6a0-0000-7000-8000-00000000000a|-3");
}
#[test]
fn a_records_version_column_is_read_off_its_type() {
let table = Table::of::<Report>();
let column = |name: &str| *table.columns.iter().find(|c| c.name == name).unwrap();
assert_eq!(column("at").version, Some(VersionKind::Instant));
assert_eq!(column("id").version, None);
assert_eq!(Report::VERSION, Some("at"));
}
#[cfg(feature = "postgres")]
#[sqlx::test(migrations = false)]
async fn a_record_is_upserted_newest_wins_and_inserted_first_wins(pool: sqlx::PgPool) {
sqlx::raw_sql("CREATE TABLE reports (id uuid PRIMARY KEY, at timestamptz NOT NULL)")
.execute(&pool)
.await
.unwrap();
let mut conn = pool.acquire().await.unwrap();
let first = Report {
id: Uuid::now_v7(),
at: chrono::DateTime::from_timestamp_micros(1_700_000_000_000_000).unwrap(),
};
let older = Report {
at: first.at - chrono::Duration::seconds(1),
..first.clone()
};
let newer = Report {
at: first.at + chrono::Duration::seconds(1),
..first.clone()
};
async fn by_key(conn: &mut Conn, report: &Report) -> Vec<Report> {
get_by_keys::<Report>(conn, &[report.key()]).await.unwrap()
}
upsert(&mut conn, vec![first.clone()]).await.unwrap();
upsert(&mut conn, vec![older.clone()]).await.unwrap();
assert_eq!(
by_key(&mut conn, &first).await,
vec![first.clone()],
"a late report"
);
upsert(&mut conn, vec![newer.clone()]).await.unwrap();
assert_eq!(by_key(&mut conn, &first).await, vec![newer.clone()]);
let other = Report {
id: Uuid::now_v7(),
..first.clone()
};
let inserted = insert_ignore(&mut conn, vec![older, other.clone()])
.await
.unwrap();
assert_eq!(inserted, [other.key()], "the one it held left as it was");
assert_eq!(by_key(&mut conn, &first).await, [newer]);
}
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Row)]
#[row(
table = "screens",
mirror = "setting",
key = "replica_id, id",
origin = "replica_id",
apply = "if_unchanged"
)]
struct Screen {
replica_id: Uuid,
id: Uuid,
name: String,
#[row(desired)]
level: i64,
#[row(version)]
at: chrono::DateTime<chrono::Utc>,
}
#[test]
fn a_setting_takes_only_its_desired_columns_and_moves_its_version() {
let at = chrono::DateTime::from_timestamp_micros(1_700_000_000_000_001).unwrap();
let held = Screen {
replica_id: Uuid::now_v7(),
id: Uuid::now_v7(),
name: "front".into(),
level: 40,
at,
};
let wanted = Screen {
replica_id: Uuid::nil(),
id: Uuid::nil(),
name: "anything".into(),
level: 70,
at: at - chrono::Duration::days(1),
};
assert_eq!(
Table::of::<Screen>()
.columns
.iter()
.filter(|c| c.desired)
.map(|c| c.name)
.collect::<Vec<_>>(),
["level"]
);
assert_eq!(Screen::APPLY, Apply::IfUnchanged);
let mut row = held.clone();
row.take_desired(wanted);
assert_eq!(
row,
Screen {
level: 70,
..held.clone()
}
);
assert_eq!(
row.version_text().as_deref(),
Some("2023-11-14T22:13:20.000001Z")
);
row.raise_version();
assert!(row.at > at, "moved past what it was");
assert_eq!(
row.at.timestamp_subsec_nanos() % 1_000,
0,
"to the microsecond"
);
}
#[test]
fn a_version_is_compared_as_its_text_to_the_microsecond() {
let nanos = chrono::DateTime::from_timestamp(1_700_000_000, 123_456_789).unwrap();
let micros = chrono::DateTime::from_timestamp(1_700_000_000, 123_456_000).unwrap();
assert_eq!(nanos.text(), micros.text(), "what PostgreSQL keeps of it");
assert_eq!(micros.text(), "2023-11-14T22:13:20.123456Z");
let east = micros.with_timezone(&chrono::FixedOffset::east_opt(3_600).unwrap());
assert_eq!(east.text(), micros.text(), "the same instant, in UTC");
assert_eq!(micros.naive_utc().text(), micros.text());
assert_eq!(41_i64.text(), "41");
assert_eq!(41_i16.raised(), 42);
assert_eq!(i32::MAX.raised(), i32::MAX);
let future = chrono::Utc::now() + chrono::Duration::days(1);
assert_eq!(
future.raised().text(),
(future + chrono::Duration::microseconds(1)).text(),
"a microsecond past a clock that is behind"
);
assert!(
micros.raised() > chrono::Utc::now() - chrono::Duration::seconds(5),
"now"
);
}
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Row)]
#[row(table = "tangled", mirror = "catalog", unmirrored = "label")]
struct Tangled {
id: Uuid,
#[sqlx(flatten)]
first: Label,
#[sqlx(flatten)]
second: Label,
}
#[test]
fn a_row_whose_flattened_parts_contradict_it_says_so() {
assert_eq!(
Table::of::<Tangled>().contradictions(),
[
"the column `label` is a field of the row twice, once in a part flattened in",
"`label` is a field of the row, in a part flattened in, and is also `unmirrored`",
]
);
assert!(Table::of::<Order>().contradictions().is_empty());
}
#[derive(sqlx::FromRow, Clone, Debug, PartialEq, crate::Row)]
#[row(table = "paths", mirror = "catalog")]
struct Paths {
id: Uuid,
bare: Option<String>,
by_std: std::option::Option<String>,
by_core: ::core::option::Option<String>,
plain: String,
}
#[test]
fn an_option_is_nullable_by_whichever_path_it_is_written() {
let nullable: Vec<_> = Table::of::<Paths>()
.columns
.iter()
.map(|c| (c.name, c.nullable))
.collect();
assert_eq!(
nullable,
[
("id", false),
("bare", true),
("by_std", true),
("by_core", true),
("plain", false)
]
);
}
#[test]
fn a_row_declares_its_table_as_the_database_names_it() {
let table = Table::of::<Order>();
assert_eq!(table.name, "order");
assert_eq!(table.schema, "Shop");
#[cfg(feature = "postgres")]
{
assert_eq!(table.qualified, r#""Shop"."order""#);
assert_eq!(table.active, Some(r#""Shop"."live orders""#));
}
#[cfg(feature = "sqlite")]
assert_eq!(table.qualified, r#""order""#);
assert_eq!(table.key, ["group", "line"]);
let column = |name: &str| *table.columns.iter().find(|c| c.name == name).unwrap();
assert_eq!(column("group").key, Some(KeyKind::Uuid));
assert_eq!(column("line").key, Some(KeyKind::Integer));
assert_eq!(column("type").field, "type");
assert_eq!(column("Total Due").field, "total");
assert!(column("Total Due").int64, "an alias of i64 is an i64");
assert!(column("note").int64 && column("note").nullable);
assert!(!column("line").int64);
assert_eq!(table.retired, Some("retired at"));
}
#[cfg(feature = "sqlite")]
#[sqlx::test(migrations = false)]
async fn a_table_named_as_sql_would_refuse_unquoted_is_written_and_read(
pool: sqlx::SqlitePool,
) {
sqlx::raw_sql(
r#"CREATE TABLE "order" ("group" BLOB NOT NULL, line INTEGER NOT NULL,
"type" TEXT NOT NULL, "Total Due" INTEGER NOT NULL, note INTEGER,
at TEXT NOT NULL, "retired at" TEXT, PRIMARY KEY ("group", line))"#,
)
.execute(&pool)
.await
.unwrap();
let mut conn = pool.acquire().await.unwrap();
let order = Order {
group: Uuid::now_v7(),
line: 1,
r#type: "retail".into(),
total: 1 << 40,
note: None,
at: chrono::Utc::now(),
};
let written = overwrite_except(&mut conn, vec![order.clone()], &[])
.await
.unwrap();
assert_eq!(written, [order.key()]);
let newer = Order {
total: 7,
at: order.at + chrono::Duration::seconds(1),
..order.clone()
};
overwrite_except(&mut conn, vec![newer.clone()], &[])
.await
.unwrap();
overwrite_except(&mut conn, vec![order.clone()], &[])
.await
.unwrap();
assert_eq!(
get_by_keys::<Order>(&mut conn, &[order.key()])
.await
.unwrap(),
vec![order.clone()],
"an older version, taken"
);
let table = Table::of::<Order>();
assert_eq!(
parity(&mut conn, &[&table])
.await
.map_err(|e| e.to_string()),
Ok(())
);
let retired = retire_by_keys::<Order>(&mut conn, &[order.key()], &[], &[])
.await
.unwrap();
assert_eq!(retired.deleted, 1);
}
}