Skip to main content

cratefield_tables_api/
batch.rs

1//! Several declared reads in one request (issue #153).
2//!
3//! A sidecar or a browser client rendering a page from four tables pays
4//! four round trips otherwise, and a round trip to a Worker is the
5//! expensive part of a read that a database answers in a millisecond.
6//!
7//! # What this does and does not buy
8//!
9//! One round trip. **Not** parallelism: the reads run in sequence against
10//! the request's one database handle, because that is what a handle is.
11//! Saying otherwise would be claiming a property the shape cannot have —
12//! and a caller who believed it would size their batches by the wrong
13//! number.
14//!
15//! # All or nothing
16//!
17//! A batch whose caller may not make one of its reads is refused whole,
18//! naming the read. The alternative — a `200` carrying a refusal per
19//! result — is a success that is not one, and every client would have to
20//! remember to look inside it. A refusal that reads as success is worse
21//! than a refusal.
22//!
23//! # The path cannot be a table
24//!
25//! `__batch` is not a legal declared table name: a name must start with a
26//! lowercase letter and may not contain `__`. So this route cannot shadow
27//! a venture's own table, and `the_batch_path_can_never_be_a_table_name`
28//! says so rather than leaving it to be noticed.
29
30use std::sync::Arc;
31
32use cratefield_core::{Database, Problem, ProblemDef, Scope, Tenancy};
33use http::{HeaderMap, StatusCode};
34use serde::Deserialize;
35use serde_json::{Map, Value, json};
36
37use crate::read::Tables;
38
39/// The path the batch is served at, under the module's own mount.
40pub const PATH: &str = "/__batch";
41
42/// The most reads one batch may ask for.
43///
44/// A cap, because a batch is a fan-out a single request controls: without
45/// one, a caller who may read one table may read it two thousand times in
46/// a request the gateway counts as one.
47pub const MAX_READS: usize = 20;
48
49/// The batch asked for more reads than one request may carry.
50pub const TOO_MANY_READS: ProblemDef = ProblemDef {
51    slug: "too-many-reads",
52    status: StatusCode::BAD_REQUEST,
53    title: "Too many reads in one batch",
54    description: "A batch carries at most 20 reads; send the rest as another batch.",
55};
56
57/// The batch asked for nothing.
58pub const NO_READS: ProblemDef = ProblemDef {
59    slug: "no-reads",
60    status: StatusCode::BAD_REQUEST,
61    title: "A batch with no reads",
62    description: "A batch names at least one read; an empty one is a request with no question.",
63};
64
65/// One read in a batch.
66#[derive(Debug, Deserialize)]
67pub struct Read {
68    /// The declared table.
69    pub table: String,
70    /// Where the page starts — the `next` from a previous read of this
71    /// table, exactly as it was given.
72    #[serde(default)]
73    pub after: Option<Value>,
74    /// `sort=column` or `sort=-column`, the same shape the query string
75    /// takes.
76    #[serde(default)]
77    pub sort: Option<String>,
78    /// Equality filters, keyed by column.
79    ///
80    /// JSON values rather than the query string's text, because a body
81    /// can carry a number as a number. The column's kind still decides
82    /// whether the value is one it can be compared against.
83    #[serde(default, rename = "where")]
84    pub filters: Map<String, Value>,
85}
86
87/// What a batch asks for.
88#[derive(Debug, Deserialize)]
89pub struct Batch {
90    /// The reads, answered in this order.
91    pub reads: Vec<Read>,
92}
93
94/// Runs every read in `batch`, or refuses the batch.
95///
96/// # Errors
97///
98/// [`NO_READS`] or [`TOO_MANY_READS`] for a batch that is not one, and
99/// the first read's own refusal otherwise — access, an undeclared table,
100/// a bad cursor or a bad filter. The refusal names which read it was:
101/// "the third read of a batch failed" is not something a caller can act
102/// on without being told which the third was.
103pub async fn run(
104    tables: &Tables,
105    conn: &dyn Database,
106    tenancy: Tenancy,
107    headers: &HeaderMap,
108    scope: &Scope,
109    batch: &Batch,
110) -> Result<Value, Problem> {
111    if batch.reads.is_empty() {
112        return Err(Problem::new(&NO_READS));
113    }
114    if batch.reads.len() > MAX_READS {
115        return Err(Problem::new(&TOO_MANY_READS).with_detail(format!(
116            "{} reads were asked for and {MAX_READS} is the most",
117            batch.reads.len()
118        )));
119    }
120
121    let mut results = Vec::with_capacity(batch.reads.len());
122    for (at, read) in batch.reads.iter().enumerate() {
123        // Each read is decided on its own: the access level of the table
124        // it names, against this caller. A batch is a way to ask several
125        // questions in one request, never a way to ask one the caller
126        // could not ask alone.
127        let filters = filters_of(read);
128        let body = crate::read::page(
129            tables,
130            conn,
131            tenancy,
132            headers,
133            scope,
134            crate::read::Asked {
135                table: &read.table,
136                after: read.after.as_ref(),
137                filters: &filters,
138                sort: crate::routes::sort_of(read.sort.as_deref()),
139            },
140        )
141        .await
142        .map_err(|problem| {
143            // The caller sent an ordered list; the answer says which of
144            // them it is about.
145            let said = problem.detail.clone();
146            problem.with_detail(match said {
147                Some(detail) => format!("read {at} (`{}`): {detail}", read.table),
148                None => format!("read {at} (`{}`)", read.table),
149            })
150        })?;
151        results.push(json!({ "table": read.table, "rows": body["rows"], "next": body["next"] }));
152    }
153    Ok(json!({ "results": results }))
154}
155
156/// One read's filters, as the query layer takes them.
157///
158/// The columns are not checked here. `page` checks them against the same
159/// declaration, and doing it twice would be two places to keep in step —
160/// the batch's job is to reshape the map, not to re-decide anything.
161fn filters_of(read: &Read) -> Vec<cratefield_tables::Filter> {
162    read.filters
163        .iter()
164        .map(|(column, value)| cratefield_tables::Filter {
165            column: column.clone(),
166            value: value.clone(),
167        })
168        .collect()
169}
170
171/// The state the route needs.
172pub type State = Arc<Tables>;