Skip to main content

magic_sync_derive/
lib.rs

1//! `#[derive(Row)]` and `#[derive(Columns)]` for [magic_sync]: one struct is
2//! one table, column for column, and says how it travels. Use them through
3//! `magic_sync`, which re-exports both; every attribute is documented there,
4//! on [`magic_sync::Row`].
5//!
6//! Everything the derive is told is checked where it is written: a key that
7//! is not a field, a column named twice, an attribute a table of its kind
8//! cannot have, a key or version of a type the engine cannot read — each is a
9//! compile error pointing at the token that says it.
10//!
11//! [magic_sync]: https://docs.rs/magic_sync
12//! [`magic_sync::Row`]: https://docs.rs/magic_sync/latest/magic_sync/derive.Row.html
13
14#![warn(missing_docs)]
15
16use proc_macro::TokenStream as Tokens;
17use proc_macro2::{Span, TokenStream};
18use quote::{quote, quote_spanned, ToTokens};
19use syn::meta::ParseNestedMeta;
20use syn::spanned::Spanned;
21use syn::{parse_macro_input, Data, DeriveInput, Field, Fields, Ident, LitStr, Type};
22
23/// Implements `magic_sync::Row` for a struct that is one table, column for
24/// column. Use it as `magic_sync::Row`: the attributes it reads (`#[row(…)]`
25/// on the struct and its fields, and the `#[sqlx(…)]` keys that name a
26/// column) are documented there.
27#[proc_macro_derive(Row, attributes(row))]
28pub fn derive_row(input: Tokens) -> Tokens {
29    let input = parse_macro_input!(input as DeriveInput);
30    row(&input)
31        .unwrap_or_else(syn::Error::into_compile_error)
32        .into()
33}
34
35/// Implements `magic_sync::Columns` for a part of a row, which the row brings
36/// in with `#[sqlx(flatten)]`. Use it as `magic_sync::Columns`: its fields take
37/// the same `#[row(…)]` flags as a row's, and the struct only `crate`.
38#[proc_macro_derive(Columns, attributes(row))]
39pub fn derive_columns(input: Tokens) -> Tokens {
40    let input = parse_macro_input!(input as DeriveInput);
41    columns_only(&input)
42        .unwrap_or_else(syn::Error::into_compile_error)
43        .into()
44}
45
46/// The keys `#[row(…)]` takes on a struct that derives `Row`.
47const ROW_KEYS: &str = "`table`, `schema`, `mirror`, `sync`, `key`, `active`, `retired`, \
48                        `origin`, `unmirrored`, `apply` or `crate`";
49
50/// PostgreSQL keeps the first 63 bytes of a longer name, and would answer
51/// for a table or column the row does not name.
52const LONGEST_NAME: usize = 63;
53
54#[derive(Default)]
55struct Args {
56    table: Option<LitStr>,
57    key: Option<LitStr>,
58    schema: Option<LitStr>,
59    mirror: Option<LitStr>,
60    sync: Option<LitStr>,
61    active: Option<LitStr>,
62    retired: Option<LitStr>,
63    origin: Option<LitStr>,
64    unmirrored: Option<LitStr>,
65    apply: Option<LitStr>,
66    krate: Option<LitStr>,
67}
68
69/// The struct's `#[row(…)]` keys. `only_crate`: a part a row flattens in,
70/// which takes `crate` and nothing else.
71fn parse_args(input: &DeriveInput, only_crate: bool) -> syn::Result<Args> {
72    let mut args = Args::default();
73    for attr in input.attrs.iter().filter(|a| a.path().is_ident("row")) {
74        attr.parse_nested_meta(|meta| {
75            let slot = match meta.path.get_ident().map(Ident::to_string).as_deref() {
76                Some("crate") => &mut args.krate,
77                Some(_) if only_crate => return Err(meta.error(
78                    "#[derive(Columns)] is a part a row flattens in: only `crate` applies to it",
79                )),
80                Some("table") => &mut args.table,
81                Some("key") => &mut args.key,
82                Some("schema") => &mut args.schema,
83                Some("mirror") => &mut args.mirror,
84                Some("sync") => &mut args.sync,
85                Some("active") => &mut args.active,
86                Some("retired") => &mut args.retired,
87                Some("origin") => &mut args.origin,
88                Some("unmirrored") => &mut args.unmirrored,
89                Some("apply") => &mut args.apply,
90                _ => return Err(meta.error(format!("expected {ROW_KEYS}"))),
91            };
92            once(slot, &meta)
93        })?;
94    }
95    Ok(args)
96}
97
98/// Sets `slot` to the literal `meta` carries, refusing it a second time.
99fn once(slot: &mut Option<LitStr>, meta: &ParseNestedMeta) -> syn::Result<()> {
100    let name = meta.path.to_token_stream().to_string();
101    if slot.is_some() {
102        return Err(meta.error(format!("`{name}` is given twice")));
103    }
104    let value = meta
105        .value()
106        .map_err(|_| meta.error(format!("`{name}` takes a value: `{name} = \"…\"`")))?;
107    *slot = Some(value.parse()?);
108    Ok(())
109}
110
111/// A name the engine writes into SQL, quoted: refused when no database could
112/// hold it as written.
113fn identifier(text: &str, at: Span) -> syn::Result<String> {
114    if text.is_empty() {
115        return Err(syn::Error::new(at, "an empty name"));
116    }
117    if text.contains('\0') {
118        return Err(syn::Error::new(at, "a name cannot hold a NUL character"));
119    }
120    if text.len() > LONGEST_NAME {
121        return Err(syn::Error::new(
122            at,
123            format!("`{text}` is longer than the {LONGEST_NAME} bytes PostgreSQL keeps of a name"),
124        ));
125    }
126    Ok(text.to_owned())
127}
128
129/// `value` as the generated code's `Option`.
130fn option(value: Option<impl ToTokens>) -> TokenStream {
131    match value {
132        Some(value) => quote!(::core::option::Option::Some(#value)),
133        None => quote!(::core::option::Option::None),
134    }
135}
136
137/// `name` quoted as SQL quotes an identifier.
138fn quoted(name: &str) -> String {
139    format!("\"{}\"", name.replace('"', "\"\""))
140}
141
142/// A comma-separated list of names, each checked, none twice.
143fn names(lit: &LitStr) -> syn::Result<Vec<String>> {
144    let mut out: Vec<String> = Vec::new();
145    for part in lit.value().split(',') {
146        let name = identifier(part.trim(), lit.span())?;
147        if out.contains(&name) {
148            return Err(syn::Error::new_spanned(
149                lit,
150                format!("`{name}` is listed twice"),
151            ));
152        }
153        out.push(name);
154    }
155    Ok(out)
156}
157
158fn krate(args: &Args) -> syn::Result<syn::Path> {
159    match &args.krate {
160        Some(lit) => lit.parse(),
161        None => Ok(syn::parse_quote!(::magic_sync)),
162    }
163}
164
165/// What `ty` holds, when it is written as the standard `Option<T>`: bare,
166/// or by its path in `std` or `core`. Read off the type as written: an alias
167/// to `Option` reads as NOT NULL, which the engine's check of the table
168/// against its row then refuses.
169fn option_inner(ty: &Type) -> Option<&Type> {
170    let Type::Path(path) = ty else { return None };
171    if path.qself.is_some() {
172        return None;
173    }
174    let names: Vec<String> = path
175        .path
176        .segments
177        .iter()
178        .map(|s| s.ident.to_string())
179        .collect();
180    let std_path = matches!(
181        names
182            .iter()
183            .map(String::as_str)
184            .collect::<Vec<_>>()
185            .as_slice(),
186        ["Option"] | ["std" | "core", "option", "Option"]
187    );
188    if !std_path {
189        return None;
190    }
191    let syn::PathArguments::AngleBracketed(args) = &path.path.segments.last()?.arguments else {
192        return None;
193    };
194    match args.args.first()? {
195        syn::GenericArgument::Type(inner) if args.args.len() == 1 => Some(inner),
196        _ => None,
197    }
198}
199
200/// The `#[sqlx(…)]` keys on a field that name or shape its column.
201#[derive(Default)]
202struct SqlxField {
203    skip: bool,
204    flatten: bool,
205    rename: Option<LitStr>,
206    try_from: Option<Type>,
207}
208
209fn sqlx_field(field: &Field) -> syn::Result<SqlxField> {
210    let mut out = SqlxField::default();
211    for attr in field.attrs.iter().filter(|a| a.path().is_ident("sqlx")) {
212        attr.parse_nested_meta(|meta| {
213            if meta.path.is_ident("skip") {
214                out.skip = true;
215            } else if meta.path.is_ident("flatten") {
216                out.flatten = true;
217            } else if meta.path.is_ident("rename") {
218                out.rename = Some(meta.value()?.parse()?);
219            } else if meta.path.is_ident("try_from") {
220                out.try_from = Some(meta.value()?.parse::<LitStr>()?.parse()?);
221            } else if meta.path.is_ident("json") {
222                return Err(meta.error("#[derive(Row)] does not support #[sqlx(json)]"));
223            } else if meta.input.peek(syn::Token![=]) {
224                let _: syn::Expr = meta.value()?.parse()?;
225            } else if meta.input.peek(syn::token::Paren) {
226                meta.parse_nested_meta(|_| Ok(()))?;
227            }
228            Ok(())
229        })?;
230    }
231    if out.skip || out.flatten {
232        let stray = out
233            .rename
234            .as_ref()
235            .map(Spanned::span)
236            .or_else(|| out.try_from.as_ref().map(Spanned::span));
237        if let Some(at) = stray {
238            return Err(syn::Error::new(
239                at,
240                "a skipped or flattened field names no column: nothing reads its `rename` or `try_from`",
241            ));
242        }
243    }
244    Ok(out)
245}
246
247/// The `#[row(…)]` flags on a field, each where it is written.
248#[derive(Default)]
249struct RowField {
250    generated: Option<Span>,
251    replica_owned: Option<Span>,
252    version: Option<Span>,
253    created: Option<Span>,
254    desired: Option<Span>,
255}
256
257impl RowField {
258    fn any(&self) -> Option<Span> {
259        self.generated
260            .or(self.replica_owned)
261            .or(self.version)
262            .or(self.created)
263            .or(self.desired)
264    }
265}
266
267fn row_field(field: &Field) -> syn::Result<RowField> {
268    let mut out = RowField::default();
269    for attr in field.attrs.iter().filter(|a| a.path().is_ident("row")) {
270        attr.parse_nested_meta(|meta| {
271            let flag =
272                match meta.path.get_ident().map(Ident::to_string).as_deref() {
273                    Some("generated") => &mut out.generated,
274                    Some("replica_owned") => &mut out.replica_owned,
275                    Some("version") => &mut out.version,
276                    Some("created") => &mut out.created,
277                    Some("desired") => &mut out.desired,
278                    _ => return Err(meta.error(
279                        "expected `version`, `created`, `replica_owned`, `generated` or `desired`",
280                    )),
281                };
282            let name = meta.path.to_token_stream().to_string();
283            if flag.is_some() {
284                return Err(meta.error(format!("`{name}` is given twice")));
285            }
286            if !meta.input.is_empty() && !meta.input.peek(syn::Token![,]) {
287                return Err(meta.error(format!("`{name}` takes no value")));
288            }
289            *flag = Some(meta.path.span());
290            Ok(())
291        })?;
292    }
293    let refuse = |at: Option<Span>, why: &str| match at {
294        Some(at) => Err(syn::Error::new(at, why)),
295        None => Ok(()),
296    };
297    if out.generated.is_some() {
298        refuse(
299            out.version
300                .or(out.created)
301                .or(out.replica_owned)
302                .or(out.desired),
303            "a #[row(generated)] column is computed by the database: nothing writes it",
304        )?;
305    }
306    if out.desired.is_some() {
307        refuse(
308            out.version.or(out.created),
309            "a #[row(desired)] column is one a wish sets: the version moves by itself, and a \
310             #[row(created)] column never moves",
311        )?;
312    }
313    if out.version.is_some() {
314        refuse(
315            out.created,
316            "a #[row(version)] column moves with every write; a #[row(created)] one never moves",
317        )?;
318    }
319    Ok(out)
320}
321
322/// A column a struct declares directly.
323struct Direct {
324    name: String,
325    /// Where the name is written: its `rename`, or the field.
326    at: Span,
327    ident: Ident,
328    ty: Type,
329    nullable: bool,
330    flags: RowField,
331}
332
333/// What `#[derive(Columns)]` and `#[derive(Row)]` share: the columns, and the
334/// `Columns` impl that lists and binds them.
335struct Declared {
336    tokens: TokenStream,
337    direct: Vec<Direct>,
338    flattened: Vec<Type>,
339}
340
341fn refuse_shape(input: &DeriveInput) -> syn::Result<&syn::FieldsNamed> {
342    if !input.generics.params.is_empty() || input.generics.where_clause.is_some() {
343        return Err(syn::Error::new_spanned(
344            &input.generics,
345            "a row is one table: it takes no generic parameters",
346        ));
347    }
348    let Data::Struct(data) = &input.data else {
349        return Err(syn::Error::new_spanned(
350            &input.ident,
351            "a row is a struct, one field per column",
352        ));
353    };
354    let Fields::Named(fields) = &data.fields else {
355        return Err(syn::Error::new_spanned(
356            &input.ident,
357            "a row is a struct with named fields, one per column",
358        ));
359    };
360    for attr in input.attrs.iter().filter(|a| a.path().is_ident("sqlx")) {
361        attr.parse_nested_meta(|meta| {
362            if meta.path.is_ident("rename_all") {
363                return Err(meta.error(
364                    "#[derive(Row)] reads column names off the fields: rename them one by one",
365                ));
366            }
367            if meta.input.peek(syn::Token![=]) {
368                let _: syn::Expr = meta.value()?.parse()?;
369            } else if meta.input.peek(syn::token::Paren) {
370                meta.parse_nested_meta(|_| Ok(()))?;
371            }
372            Ok(())
373        })?;
374    }
375    Ok(fields)
376}
377
378/// `key`: the row's key columns, whose kind the engine reads off their type.
379fn declared(input: &DeriveInput, krate: &syn::Path, key: &[String]) -> syn::Result<Declared> {
380    let fields = refuse_shape(input)?;
381    let private = quote!(#krate::__private);
382    let mut push = Vec::new();
383    let mut bind = Vec::new();
384    let mut direct: Vec<Direct> = Vec::new();
385    let mut flattened: Vec<Type> = Vec::new();
386    for field in &fields.named {
387        let sqlx = sqlx_field(field)?;
388        let flags = row_field(field)?;
389        if let Some(at) = flags.any().filter(|_| sqlx.skip || sqlx.flatten) {
390            return Err(syn::Error::new(
391                at,
392                "a #[row(…)] field names a column; a skipped or flattened field is none",
393            ));
394        }
395        if sqlx.skip {
396            continue;
397        }
398        let ty = &field.ty;
399        let ident = field.ident.clone().expect("a named field");
400        if sqlx.flatten {
401            push.push(quote_spanned! {ty.span()=>
402                <#ty as #private::Columns>::columns(out);
403            });
404            bind.push(quote_spanned! {ty.span()=>
405                <#ty as #private::Columns>::push_binds(self.#ident, sep);
406            });
407            flattened.push(ty.clone());
408            continue;
409        }
410        let field_name = ident.to_string().trim_start_matches("r#").to_owned();
411        let (name, at) = match &sqlx.rename {
412            Some(lit) => (identifier(&lit.value(), lit.span())?, lit.span()),
413            None => (identifier(&field_name, ident.span())?, ident.span()),
414        };
415        if let Some(earlier) = direct.iter().find(|d| d.name == name) {
416            return Err(syn::Error::new(
417                at,
418                format!(
419                    "the column `{name}` is already the field `{}`",
420                    earlier.ident
421                ),
422            ));
423        }
424        let nullable = option_inner(ty).is_some();
425        if let Some(version) = flags.version.filter(|_| nullable) {
426            return Err(syn::Error::new(
427                version,
428                "#[row(version)] orders a row's versions: it cannot be NULL",
429            ));
430        }
431        if flags.generated.is_none() {
432            bind.push(quote!(sep.push_bind(self.#ident);));
433        }
434        let decoded = sqlx.try_from.as_ref().unwrap_or(ty);
435        let key = if key.contains(&name) {
436            quote_spanned! {ty.span()=>
437                ::core::option::Option::Some(<#ty as #krate::row::KeyValue>::KIND)
438            }
439        } else {
440            quote!(::core::option::Option::None)
441        };
442        let version = if flags.version.is_some() {
443            quote_spanned! {ty.span()=>
444                ::core::option::Option::Some(<#ty as #krate::row::Version>::KIND)
445            }
446        } else {
447            quote!(::core::option::Option::None)
448        };
449        let generated = flags.generated.is_some();
450        let replica_owned = flags.replica_owned.is_some();
451        let desired = flags.desired.is_some();
452        push.push(quote! {
453            out.push(#private::Column {
454                name: #name,
455                field: #field_name,
456                nullable: #nullable,
457                generated: #generated,
458                replica_owned: #replica_owned,
459                desired: #desired,
460                key: #key,
461                version: #version,
462                int64: #private::is_int64::<#ty>(),
463                compatible: |info| {
464                    <#decoded as #private::sqlx::Type<#private::Db>>::compatible(info)
465                },
466            });
467        });
468        direct.push(Direct {
469            name,
470            at,
471            ident,
472            ty: ty.clone(),
473            nullable,
474            flags,
475        });
476    }
477
478    // The one column marked so, here or in a part flattened in.
479    let role = |pick: fn(&RowField) -> Option<Span>, which: TokenStream, what: &str| {
480        let marked: Vec<&Direct> = direct.iter().filter(|d| pick(&d.flags).is_some()).collect();
481        if let [_, second, ..] = marked.as_slice() {
482            return Err(syn::Error::new(
483                pick(&second.flags).unwrap_or(second.at),
484                format!("one column at most is #[row({what})]"),
485            ));
486        }
487        let here = option(marked.first().map(|column| &column.name));
488        let parts = flattened
489            .iter()
490            .map(|part| quote!(<#part as #private::Columns>::#which));
491        let refusal =
492            format!("one column at most is #[row({what})], here or in a part flattened in");
493        Ok(quote! {
494            {
495                let found = [#here, #(#parts),*];
496                if #private::count(&found) > 1 {
497                    ::core::panic!(#refusal)
498                }
499                #private::first(&found)
500            }
501        })
502    };
503    let version = role(|f| f.version, quote!(VERSION), "version")?;
504    let created = role(|f| f.created, quote!(CREATED), "created")?;
505    let owned_here = direct.iter().any(|d| d.flags.replica_owned.is_some());
506    let owned_parts = flattened
507        .iter()
508        .map(|part| quote!(|| <#part as #private::Columns>::REPLICA_OWNED));
509
510    let ident = &input.ident;
511    let tokens = quote! {
512        // The roles are settled where the struct is declared, not where a
513        // row of it is first used.
514        const _: () = {
515            let _ = <#ident as #private::Columns>::VERSION;
516            let _ = <#ident as #private::Columns>::CREATED;
517        };
518
519        impl #private::Sealed for #ident {}
520
521        impl #private::Columns for #ident {
522            const VERSION: ::core::option::Option<&'static ::core::primitive::str> = #version;
523            const CREATED: ::core::option::Option<&'static ::core::primitive::str> = #created;
524            const REPLICA_OWNED: ::core::primitive::bool = #owned_here #(#owned_parts)*;
525
526            fn columns(out: &mut ::std::vec::Vec<#private::Column>) {
527                #(#push)*
528            }
529
530            fn push_binds<'args>(
531                self,
532                sep: &mut #private::sqlx::query_builder::Separated<'_, 'args, #private::Db, &'static ::core::primitive::str>,
533            ) {
534                #(#bind)*
535            }
536        }
537    };
538    Ok(Declared {
539        tokens,
540        direct,
541        flattened,
542    })
543}
544
545fn columns_only(input: &DeriveInput) -> syn::Result<TokenStream> {
546    let args = parse_args(input, true)?;
547    let declared = declared(input, &krate(&args)?, &[])?;
548    if let Some(at) = declared.direct.iter().find_map(|d| d.flags.desired) {
549        return Err(syn::Error::new(
550            at,
551            "#[row(desired)] is a field of the setting itself, not of a part flattened in",
552        ));
553    }
554    Ok(declared.tokens)
555}
556
557#[derive(Clone, Copy, PartialEq, Eq)]
558enum Mirror {
559    Catalog,
560    Record,
561    Both,
562    Setting,
563}
564
565impl Mirror {
566    fn parse(lit: &LitStr) -> syn::Result<Self> {
567        Ok(match lit.value().as_str() {
568            "catalog" => Mirror::Catalog,
569            "record" => Mirror::Record,
570            "both" => Mirror::Both,
571            "setting" => Mirror::Setting,
572            other => {
573                return Err(syn::Error::new_spanned(
574                    lit,
575                    format!(
576                        "mirror = \"{other}\": expected \"catalog\", \"record\", \"both\" or \"setting\""
577                    ),
578                ))
579            }
580        })
581    }
582
583    fn variant(self) -> TokenStream {
584        match self {
585            Mirror::Catalog => quote!(Catalog),
586            Mirror::Record => quote!(Record),
587            Mirror::Both => quote!(Both),
588            Mirror::Setting => quote!(Setting),
589        }
590    }
591
592    /// Whether the server sends the table's rows down.
593    fn sent_down(self) -> bool {
594        matches!(self, Mirror::Catalog | Mirror::Both)
595    }
596
597    /// Whether a replica's rows of the table go up, ordered by a version.
598    fn versioned(self) -> bool {
599        matches!(self, Mirror::Record | Mirror::Setting)
600    }
601}
602
603/// Refuses `lit`, given on a table that is not sent down.
604fn only_sent_down(lit: Option<&LitStr>, mirror: Mirror, what: &str) -> syn::Result<()> {
605    match lit {
606        Some(lit) if !mirror.sent_down() => Err(syn::Error::new_spanned(
607            lit,
608            format!("only a table the server sends down (`catalog` or `both`) has {what}"),
609        )),
610        _ => Ok(()),
611    }
612}
613
614fn row(input: &DeriveInput) -> syn::Result<TokenStream> {
615    let args = parse_args(input, false)?;
616    let krate = krate(&args)?;
617    let private = quote!(#krate::__private);
618    let ident = &input.ident;
619    let missing = |what: &str| {
620        syn::Error::new(
621            Span::call_site(),
622            format!("#[derive(Row)] needs #[row({what} = \"…\")]"),
623        )
624    };
625    let table_lit = args.table.as_ref().ok_or_else(|| missing("table"))?;
626    let table = identifier(&table_lit.value(), table_lit.span())?;
627    let schema = match &args.schema {
628        Some(lit) => identifier(&lit.value(), lit.span())?,
629        None => "public".to_owned(),
630    };
631    let mirror = Mirror::parse(args.mirror.as_ref().ok_or_else(|| missing("mirror"))?)?;
632    let key = match &args.key {
633        Some(lit) => names(lit)?,
634        None => vec!["id".to_owned()],
635    };
636    let Declared {
637        tokens: columns,
638        direct,
639        flattened,
640    } = declared(input, &krate, &key)?;
641    let column = |name: &str| direct.iter().find(|d| d.name == name);
642
643    if mirror != Mirror::Both {
644        if let Some(at) = direct.iter().find_map(|d| d.flags.replica_owned) {
645            return Err(syn::Error::new(
646                at,
647                "#[row(replica_owned)] splits a table written on both sides: needs `mirror = \"both\"`",
648            ));
649        }
650    }
651    if !mirror.versioned() {
652        if let Some(at) = direct.iter().find_map(|d| d.flags.version) {
653            return Err(syn::Error::new(
654                at,
655                "#[row(version)] orders the reports of a replica's table: only a `record` or a \
656                 `setting` has one, and a replica takes what the server sends whatever its version",
657            ));
658        }
659    }
660    if mirror != Mirror::Setting {
661        if let Some(at) = direct.iter().find_map(|d| d.flags.desired) {
662            return Err(syn::Error::new(
663                at,
664                "#[row(desired)] is a column the server may wish a replica's setting to hold: \
665                 needs `mirror = \"setting\"`",
666            ));
667        }
668        if let Some(lit) = &args.apply {
669            return Err(syn::Error::new_spanned(
670                lit,
671                "only a `setting` has an `apply` rule: it says which wish a replica carries out",
672            ));
673        }
674    }
675    let (route, mut checks) = route(args.sync.as_ref(), mirror, &direct, &krate)?;
676    only_sent_down(args.active.as_ref(), mirror, "an `active` relation")?;
677    only_sent_down(args.retired.as_ref(), mirror, "a `retired` column")?;
678
679    let key_at = args.key.as_ref().map_or(ident.span(), LitStr::span);
680    let mut key_columns = Vec::new();
681    for name in &key {
682        let Some(column) = column(name) else {
683            let why = if flattened.is_empty() {
684                format!("the key column `{name}` is not a field of the row")
685            } else {
686                format!("the key column `{name}` is not a field of the row itself: a key is not flattened in")
687            };
688            return Err(syn::Error::new(key_at, why));
689        };
690        if column.nullable {
691            return Err(syn::Error::new(column.at, "a key column cannot be NULL"));
692        }
693        if let Some(at) = column.flags.any() {
694            return Err(syn::Error::new(
695                at,
696                "a key column is the row's identity: it is not generated, owned by a replica, \
697                 a version, a creation time or desired",
698            ));
699        }
700        key_columns.push(column);
701    }
702
703    if let Some(lit) = &args.origin {
704        let origin = identifier(&lit.value(), lit.span())?;
705        if matches!(mirror, Mirror::Catalog) {
706            return Err(syn::Error::new_spanned(
707                lit,
708                "only a table a replica writes (`record`, `both` or `setting`) has an `origin`",
709            ));
710        }
711        let Some(column) = key_columns.iter().find(|c| c.name == origin) else {
712            return Err(syn::Error::new_spanned(
713                lit,
714                format!("origin = \"{origin}\": the replica a record belongs to is a key column"),
715            ));
716        };
717        let ty = &column.ty;
718        let refusal = format!("origin = \"{origin}\": the replica a record belongs to is a uuid");
719        checks.push(quote_spanned! {lit.span()=>
720            const _: () = ::core::assert!(
721                ::core::matches!(<#ty as #krate::row::KeyValue>::KIND, #krate::row::KeyKind::Uuid),
722                #refusal,
723            );
724        });
725    }
726    let retired = match &args.retired {
727        Some(lit) => Some(identifier(&lit.value(), lit.span())?),
728        None => None,
729    };
730    let unmirrored = match &args.unmirrored {
731        Some(lit) => {
732            let names = names(lit)?;
733            for name in &names {
734                if column(name).is_some() {
735                    return Err(syn::Error::new_spanned(
736                        lit,
737                        format!("`{name}` is a field of the row: a column the row leaves out is not one of its fields"),
738                    ));
739                }
740                if retired.as_ref() == Some(name) {
741                    return Err(syn::Error::new_spanned(
742                        lit,
743                        format!("`{name}` is the retired column already"),
744                    ));
745                }
746            }
747            names
748        }
749        None => Vec::new(),
750    };
751    let active = match &args.active {
752        Some(lit) => {
753            let text = lit.value();
754            let parts: Vec<&str> = text.split('.').collect();
755            if parts.len() > 2 {
756                return Err(syn::Error::new_spanned(
757                    lit,
758                    format!("active = \"{text}\": expected `view` or `schema.view`"),
759                ));
760            }
761            let quoted: Vec<String> = parts
762                .into_iter()
763                .map(|p| identifier(p, lit.span()).map(|p| quoted(&p)))
764                .collect::<syn::Result<_>>()?;
765            Some(quoted.join("."))
766        }
767        None => None,
768    };
769    if !mirror.versioned() {
770        let refusal = "#[row(version)] in a part flattened in orders the reports of a replica's \
771                       table: only a `record` or a `setting` has one";
772        checks.extend(flattened.iter().map(|part| {
773            quote_spanned! {part.span()=>
774                const _: () = ::core::assert!(<#part as #private::Columns>::VERSION.is_none(), #refusal);
775            }
776        }));
777    }
778    if mirror != Mirror::Both {
779        let refusal = "#[row(replica_owned)] in a part flattened in splits a table written on \
780                       both sides: the row needs `mirror = \"both\"`";
781        checks.extend(flattened.iter().map(|part| {
782            quote_spanned! {part.span()=>
783                const _: () = ::core::assert!(!<#part as #private::Columns>::REPLICA_OWNED, #refusal);
784            }
785        }));
786    }
787
788    let setting = if mirror == Mirror::Setting {
789        Some(setting(&args, ident, &direct, &krate)?)
790    } else {
791        None
792    };
793
794    let active = option(active);
795    let retired = option(retired);
796    let origin = option(args.origin.as_ref().map(LitStr::value));
797    let variant = mirror.variant();
798    let (quoted_schema, quoted_table) = (quoted(&schema), quoted(&table));
799    let version_text = match direct.iter().find(|d| d.flags.version.is_some()) {
800        Some(Direct { ident, ty, .. }) => quote_spanned! {ty.span()=>
801            ::core::option::Option::Some(<#ty as #krate::row::Version>::text(&self.#ident))
802        },
803        None => quote!(::core::option::Option::None),
804    };
805    let key_parts = key_columns.iter().map(|c| {
806        let (ident, ty) = (&c.ident, &c.ty);
807        quote_spanned! {ty.span()=>
808            <#ty as #krate::row::KeyValue>::key_part(&self.#ident)
809        }
810    });
811
812    Ok(quote! {
813        #columns
814
815        impl #private::Row for #ident {
816            const TABLE: &'static ::core::primitive::str = #table;
817            const SCHEMA: &'static ::core::primitive::str = #schema;
818            const KEY: &'static [&'static ::core::primitive::str] = &[#(#key),*];
819            const MIRROR: #krate::row::Mirror = #krate::row::Mirror::#variant;
820            const QUALIFIED: &'static ::core::primitive::str = #krate::__qualified!(#quoted_schema, #quoted_table);
821            const ROUTE: #private::Route = #route;
822            const ACTIVE: ::core::option::Option<&'static ::core::primitive::str> = #active;
823            const RETIRED: ::core::option::Option<&'static ::core::primitive::str> = #retired;
824            const UNMIRRORED: &'static [&'static ::core::primitive::str] = &[#(#unmirrored),*];
825            const ORIGIN: ::core::option::Option<&'static ::core::primitive::str> = #origin;
826
827            fn key(&self) -> #krate::row::Key {
828                ::std::vec![#(#key_parts),*]
829            }
830
831            fn version_text(&self) -> ::core::option::Option<::std::string::String> {
832                #version_text
833            }
834        }
835
836        #(#checks)*
837
838        #setting
839    })
840}
841
842/// A setting's `Setting` impl: its desired columns, its `apply` rule, and
843/// what the engine does to a row of it when a wish is carried out. A setting
844/// needs an `origin` (where a wish goes), a version of its own (what a wish
845/// is based on) and at least one desired column.
846fn setting(
847    args: &Args,
848    ident: &Ident,
849    direct: &[Direct],
850    krate: &syn::Path,
851) -> syn::Result<TokenStream> {
852    let refuse = |why: &str| syn::Error::new_spanned(ident, why);
853    if args.origin.is_none() {
854        return Err(refuse(
855            "a setting needs `origin = \"…\"`: the key column naming the replica it belongs to, \
856             which is where a wish of it goes",
857        ));
858    }
859    let Some(version) = direct.iter().find(|d| d.flags.version.is_some()) else {
860        return Err(refuse(
861            "a setting needs a #[row(version)] field of its own: what a wish is based on, and what \
862             moves when one is carried out",
863        ));
864    };
865    let desired: Vec<&Direct> = direct
866        .iter()
867        .filter(|d| d.flags.desired.is_some())
868        .collect();
869    if desired.is_empty() {
870        return Err(refuse(
871            "a setting needs at least one #[row(desired)] field: a column a wish may set",
872        ));
873    }
874    let apply = match &args.apply {
875        None => quote!(Always),
876        Some(lit) => match lit.value().as_str() {
877            "always" => quote!(Always),
878            "if_unchanged" => quote!(IfUnchanged),
879            other => {
880                return Err(syn::Error::new_spanned(
881                    lit,
882                    format!("apply = \"{other}\": expected \"always\" or \"if_unchanged\""),
883                ))
884            }
885        },
886    };
887    let fields = desired.iter().map(|d| &d.ident);
888    let (version_ident, version_ty) = (&version.ident, &version.ty);
889    Ok(quote! {
890        impl #krate::row::Setting for #ident {
891            const APPLY: #krate::row::Apply = #krate::row::Apply::#apply;
892
893            fn take_desired(&mut self, from: Self) {
894                #(self.#fields = from.#fields;)*
895            }
896
897            fn raise_version(&mut self) {
898                self.#version_ident = <#version_ty as #krate::row::Version>::raised(&self.#version_ident);
899            }
900        }
901    })
902}
903
904/// `sync = "…"`: how a table's rows find the replicas that hold them, and
905/// the checks of the types of the columns it names, which are the row's own.
906fn route(
907    sync: Option<&LitStr>,
908    mirror: Mirror,
909    direct: &[Direct],
910    krate: &syn::Path,
911) -> syn::Result<(TokenStream, Vec<TokenStream>)> {
912    let private = quote!(#krate::__private);
913    let Some(lit) = sync else {
914        return Ok((quote!(#private::Route::Referenced), Vec::new()));
915    };
916    if !mirror.sent_down() {
917        return Err(syn::Error::new_spanned(
918            lit,
919            "only a table the server sends down (`catalog` or `both`) takes a `sync` strategy",
920        ));
921    }
922    let text = lit.value();
923    let bad = |why: &str| syn::Error::new_spanned(lit, format!("sync = \"{text}\": {why}"));
924    let (name, inner) = match text.split_once('(') {
925        Some((name, rest)) => {
926            let inner = rest
927                .strip_suffix(')')
928                .ok_or_else(|| bad("missing the closing parenthesis"))?;
929            (name.trim(), Some(inner))
930        }
931        None => (text.trim(), None),
932    };
933    let column = |inner: Option<&str>| -> syn::Result<&Direct> {
934        let arg = inner.map(str::trim).unwrap_or_default();
935        if arg.is_empty() || arg.contains(',') || arg.contains('=') {
936            return Err(bad("expected one column name"));
937        }
938        let name = identifier(arg, lit.span())?;
939        direct
940            .iter()
941            .find(|d| d.name == name)
942            .ok_or_else(|| bad(&format!("`{name}` is not a field of the row")))
943    };
944    // The column holds a uuid or an integer, or is nullable.
945    let of_a_key = |column: &Direct| {
946        let ty = option_inner(&column.ty).unwrap_or(&column.ty);
947        quote_spanned! {lit.span()=>
948            const _: #krate::row::KeyKind = <#ty as #krate::row::KeyValue>::KIND;
949        }
950    };
951    Ok(match name {
952        "broadcast" if inner.is_none() => (quote!(#private::Route::Broadcast), Vec::new()),
953        "assigned" => {
954            let column = column(inner)?;
955            let name = &column.name;
956            let ty = option_inner(&column.ty).unwrap_or(&column.ty);
957            let refusal = format!("sync = \"{text}\": the replica a row is sent to is a uuid");
958            let check = quote_spanned! {lit.span()=>
959                const _: () = ::core::assert!(
960                    ::core::matches!(<#ty as #krate::row::KeyValue>::KIND, #krate::row::KeyKind::Uuid),
961                    #refusal,
962                );
963            };
964            (quote!(#private::Route::Assigned(#name)), vec![check])
965        }
966        "follows" => {
967            let column = column(inner)?;
968            let name = &column.name;
969            (
970                quote!(#private::Route::Follows(#name)),
971                vec![of_a_key(column)],
972            )
973        }
974        "grouped" => {
975            let mut members = None;
976            let mut assigned = None;
977            for part in inner.unwrap_or_default().split(',') {
978                let expected = || bad("expected `members = …, assigned = …`");
979                let (key, value) = part.split_once('=').ok_or_else(expected)?;
980                let value = identifier(value.trim(), lit.span())?;
981                let slot = match key.trim() {
982                    "members" => &mut members,
983                    "assigned" => &mut assigned,
984                    _ => return Err(expected()),
985                };
986                if slot.replace(value).is_some() {
987                    return Err(bad(&format!("`{}` is given twice", key.trim())));
988                }
989            }
990            let (Some(members), Some(assigned)) = (members, assigned) else {
991                return Err(bad("expected `members = …, assigned = …`"));
992            };
993            (
994                quote!(#private::Route::Grouped {
995                    members: #members,
996                    assigned: #assigned,
997                }),
998                Vec::new(),
999            )
1000        }
1001        _ => {
1002            return Err(bad(
1003                "expected `broadcast`, `assigned(col)`, `grouped(members = …, assigned = …)` or `follows(col)`",
1004            ))
1005        }
1006    })
1007}