postrust_server/app.rs
1//! Request handling.
2
3use crate::state::AppState;
4use axum::{
5 body::Body,
6 extract::{Request, State},
7 http::StatusCode,
8 response::{IntoResponse, Response},
9};
10use postrust_auth::authenticate;
11use postrust_core::{
12 create_action_plan, parse_request, ActionPlan, ApiRequest, CallPlan, DbActionPlan,
13};
14use postrust_response::{format_response, QueryResult, Response as PgrstResponse};
15use std::sync::Arc;
16use tracing::{debug, error};
17
18/// Column carrying the parent's whole row, for computed relationships.
19///
20/// A computed relationship is a function taking the parent row. By the time an
21/// embed expression runs, the parent is a derived table, and a derived table's
22/// alias has type `record` -- which PostgreSQL will not cast to the table's
23/// composite type. So the row is captured one level down, where the real table
24/// is still in scope and a bare reference to it yields the composite, and the
25/// embed expression reads it from there. It is stripped from the response like
26/// any other column added for embedding.
27///
28/// Defined in `postrust-core` beside the `RETURNING` that emits it for a
29/// mutation, so the two spellings cannot drift.
30use postrust_core::query::PARENT_ROW_COLUMN;
31
32/// Marks an embedded object whose columns belong to its parent.
33///
34/// A spread has no key of its own in the response -- its columns land in the
35/// parent object. In SQL it is still one JSON column like any other embed, so
36/// it is given a name nothing can collide with and dissolved into its parent
37/// once the rows are JSON. Doing it there rather than in SQL is what lets a
38/// spread nest inside another, and lets it spread a computed relationship.
39const SPREAD_KEY_PREFIX: &str = "pgrst_spread_";
40const PARENT_ROW_COLUMN_REF: &str = "\"src\".\"pgrst_parent_row\"";
41
42/// Main request handler.
43pub async fn handle_request(State(state): State<Arc<AppState>>, request: Request) -> Response {
44 let method = request.method().clone();
45 let path = request.uri().path().to_string();
46
47 debug!("{} {}", method, path);
48
49 let verbatim_db_errors = state.config.compat_mode;
50
51 match process_request(state, request).await {
52 Ok(response) => response.into_response(),
53 Err(e) => error_response(e, verbatim_db_errors).into_response(),
54 }
55}
56
57/// Process a request and return a response.
58async fn process_request(
59 state: Arc<AppState>,
60 request: Request,
61) -> Result<Response, postrust_core::Error> {
62 // Extract auth header
63 let auth_header = request
64 .headers()
65 .get("authorization")
66 .and_then(|v| v.to_str().ok());
67
68 // Authenticate, through the token cache where one is configured.
69 let auth_result = match &state.jwt_cache {
70 Some(cache) => cache.authenticate(auth_header, &state.jwt_config),
71 None => authenticate(auth_header, &state.jwt_config),
72 }
73 .map_err(jwt_error)?;
74
75 debug!("Authenticated as role: {}", auth_result.role);
76
77 // Parse request.
78 //
79 // `max_body_size`, not a constant, and not `DefaultBodyLimit` either: that
80 // layer works by setting an extension the body *extractors* read, and this
81 // handler takes the whole `Request` and reads the body itself. The limit
82 // was hardcoded at 10 MiB here, so `PGRST_MAX_BODY_SIZE` was configuring
83 // nothing on this path however it was set.
84 let (parts, body) = request.into_parts();
85 // Still `InvalidBody`, so still a 400: `to_bytes` reports a body that
86 // exceeded the limit and a connection that died mid-body as the same
87 // error, and answering 413 to a truncated upload would be a worse lie than
88 // the 400.
89 let body_bytes = axum::body::to_bytes(body, state.config.max_body_size)
90 .await
91 .map_err(|e| postrust_core::Error::InvalidBody(e.to_string()))?;
92
93 // Build HTTP request for parsing
94 let mut builder = http::Request::builder()
95 .method(parts.method.clone())
96 .uri(parts.uri.clone());
97
98 for (key, value) in &parts.headers {
99 builder = builder.header(key, value);
100 }
101
102 let http_request = builder
103 .body(body_bytes.clone())
104 .map_err(|e| postrust_core::Error::Internal(e.to_string()))?;
105
106 // Parse API request
107 let mut api_request = parse_request(
108 &http_request,
109 state.default_schema(),
110 state.schemas(),
111 state.config.db_max_rows,
112 )?;
113
114 // Parse payload
115 if !body_bytes.is_empty() {
116 let payload = postrust_core::api_request::payload::parse_payload(
117 body_bytes,
118 &api_request.content_media_type,
119 )?;
120 api_request.payload = payload;
121 }
122
123 // A body that names different columns row by row cannot be written by one
124 // statement. Skipped when `?columns=` says which columns to write, since
125 // then the rows are free to differ.
126 if api_request.query_params.columns.is_none() {
127 if let Some(payload) = &api_request.payload {
128 postrust_core::api_request::payload::validate_uniform_keys(payload)?;
129 }
130 }
131
132 // A write with nothing to write is refused here rather than left to
133 // produce an `UPDATE ... SET` with no assignments, which PostgreSQL
134 // reports as a syntax error -- true, and no use at all to the client.
135 if api_request.payload.is_none() && needs_payload(&api_request) {
136 return Err(postrust_core::Error::InvalidBody(
137 "Empty or invalid json".into(),
138 ));
139 }
140
141 // `application/vnd.pgrst.plan` asks for the query plan instead of the
142 // result. It is off unless a server turns it on -- it says a great deal
143 // about the schema and the data -- and a server that has not is required
144 // to say so rather than answer with something else.
145 //
146 // Checked after the body, not before it: a request that could not be read
147 // at all has not got as far as asking for anything, and PostgREST answers
148 // it with what is wrong with the request rather than with what the server
149 // declines to serve.
150 if let Some(plan) = requested_plan(&api_request) {
151 return Err(postrust_core::Error::InvalidMediaType(plan));
152 }
153
154 // Get schema cache
155 let schema_cache = state.schema_cache().await;
156
157 // Embedding joins on a column of the parent row, so that column has to be
158 // selected even when the client did not ask for it. Any column added this
159 // way is removed from the response once the embed is attached.
160 let added_join_columns = add_embed_join_columns(&mut api_request, &schema_cache)?;
161
162 if !state.config.db_aggregates_enabled && selects_an_aggregate(&api_request) {
163 return Err(postrust_core::Error::AggregatesNotAllowed);
164 }
165
166 // Create execution plan
167 let plan = create_action_plan(&api_request, &schema_cache)?;
168
169 // Execute plan
170 let result = execute_plan(
171 &state,
172 &api_request,
173 &plan,
174 &auth_result,
175 &added_join_columns,
176 )
177 .await?;
178
179 // Format response
180 // A singular response the result cannot satisfy is the client's business,
181 // not an internal failure: it asked for one object and the query answered
182 // with some other number of rows.
183 let response = format_response(&api_request, &result).map_err(|e| match e {
184 postrust_response::FormatError::MultipleRows | postrust_response::FormatError::NotFound => {
185 // How many rows there were, not how many were kept: a write the
186 // caller wanted no representation from still touched them, and
187 // that count is what the client got wrong.
188 postrust_core::Error::NotSingular {
189 rows: result.rows.len().max(result.affected),
190 }
191 }
192 other => postrust_core::Error::Internal(other.to_string()),
193 })?;
194
195 Ok(build_response(response))
196}
197
198/// The plan media type a request asked for, when that is all it will accept.
199///
200/// A request that would also take JSON gets JSON; one that will take only the
201/// plan is refused, and the message names what it asked for.
202fn requested_plan(api_request: &ApiRequest) -> Option<String> {
203 use postrust_core::api_request::MediaType;
204
205 // Named back with their parameters, since that is what was asked for: a
206 // plan for CSV and a plan for JSON are different requests, and a client
207 // told only that "application/vnd.pgrst.plan+json" is unavailable has not
208 // been told which of its requests was refused.
209 let accepted: Vec<String> = api_request
210 .accept_media_types
211 .iter()
212 .filter(|media| matches!(media, MediaType::Plan { .. }))
213 .map(MediaType::to_mime)
214 .collect();
215
216 match accepted.len() == api_request.accept_media_types.len() && !accepted.is_empty() {
217 true => Some(accepted.join(", ")),
218 false => None,
219 }
220}
221
222/// Which kind of mutation this request is, if it is one.
223fn mutation_kind(api_request: &ApiRequest) -> Option<postrust_core::api_request::Mutation> {
224 use postrust_core::api_request::{Action, DbAction};
225
226 match &api_request.action {
227 Action::Db(DbAction::RelationMut { mutation, .. }) => Some(mutation.clone()),
228 _ => None,
229 }
230}
231
232/// Split a leading `WITH ... AS (...)` off the front of a statement.
233///
234/// Returns the clause -- with its trailing space, or empty when there is none
235/// -- and the statement that follows it. A `WITH` containing an `INSERT`,
236/// `UPDATE` or `DELETE` may only appear at the top level, so anything that
237/// wraps the statement has to wrap what follows the clause and put the clause
238/// back in front.
239fn split_leading_cte(sql: &str) -> (String, &str) {
240 let Some(open) = sql.strip_prefix("WITH ").and_then(|rest| rest.find('(')) else {
241 return (String::new(), sql);
242 };
243 let open = open + "WITH ".len();
244
245 let mut depth = 0usize;
246 let mut quoted = None::<char>;
247 for (index, ch) in sql[open..].char_indices() {
248 match (quoted, ch) {
249 (Some(quote), ch) if ch == quote => quoted = None,
250 (Some(_), _) => {}
251 (None, '\'') | (None, '"') => quoted = Some(ch),
252 (None, '(') => depth += 1,
253 (None, ')') => {
254 depth -= 1;
255 if depth == 0 {
256 let end = open + index + 1;
257 return (format!("{} ", &sql[..end]), sql[end..].trim_start());
258 }
259 }
260 _ => {}
261 }
262 }
263
264 (String::new(), sql)
265}
266
267/// Render a map of strings as a JSON object.
268///
269/// PostgreSQL will not accept every header name as a setting name -- a `-` is
270/// not allowed in one from version 14 -- so the whole map goes into a single
271/// setting as JSON and a function picks out what it wants with `->>`.
272fn json_object<'a>(entries: impl IntoIterator<Item = (&'a String, &'a String)>) -> String {
273 serde_json::Value::Object(
274 entries
275 .into_iter()
276 .map(|(name, value)| (name.clone(), serde_json::Value::String(value.clone())))
277 .collect(),
278 )
279 .to_string()
280}
281
282/// The prefix given to primary key columns added only to build `Location`.
283const LOCATION_KEY_PREFIX: &str = "pgrst_location_";
284
285/// The column a media handler's statement carries its row count in.
286///
287/// Named like the other bookkeeping columns so it cannot collide with anything
288/// the request selected. Never reaches the client: the response body is the
289/// rendered value, read from the first column.
290const MEDIA_ROW_COUNT_COLUMN: &str = "pgrst_media_rows";
291
292/// Whether this request writes the row its URL names.
293fn is_upsert(api_request: &ApiRequest) -> bool {
294 use postrust_core::api_request::{Action, DbAction, Mutation};
295
296 matches!(
297 &api_request.action,
298 Action::Db(DbAction::RelationMut {
299 mutation: Mutation::SingleUpsert,
300 ..
301 })
302 )
303}
304
305/// The `Location` of a newly created row, and removal of the key columns that
306/// were added to find it.
307///
308/// `/projects?id=eq.7` -- the address the row can be read back from, which is
309/// what a 201 is required to give. A key column that came back null is
310/// addressed with `is.null`, since `eq.` would match nothing.
311fn build_location(
312 api_request: &ApiRequest,
313 rows: &mut [serde_json::Value],
314 keys: &[String],
315) -> Option<String> {
316 if keys.is_empty() {
317 return None;
318 }
319
320 let table = api_request.path.rsplit('/').next()?;
321 let mut conditions = Vec::with_capacity(keys.len());
322
323 for key in keys {
324 let value = rows
325 .first()?
326 .get(key)
327 .or_else(|| rows.first()?.get(format!("{}{}", LOCATION_KEY_PREFIX, key)))?;
328 conditions.push(match value {
329 serde_json::Value::Null => format!("{}=is.null", urlencode(key)),
330 serde_json::Value::String(text) => {
331 format!("{}=eq.{}", urlencode(key), urlencode(text))
332 }
333 other => format!("{}=eq.{}", urlencode(key), urlencode(&other.to_string())),
334 });
335 }
336
337 for row in rows.iter_mut() {
338 if let Some(object) = row.as_object_mut() {
339 for key in keys {
340 object.remove(&format!("{}{}", LOCATION_KEY_PREFIX, key));
341 }
342 }
343 }
344
345 Some(format!("/{}?{}", table, conditions.join("&")))
346}
347
348/// Percent-encode everything a query-string value may not carry literally.
349fn urlencode(value: &str) -> String {
350 let mut encoded = String::with_capacity(value.len());
351 for byte in value.as_bytes() {
352 match byte {
353 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
354 encoded.push(*byte as char)
355 }
356 other => encoded.push_str(&format!("%{:02X}", other)),
357 }
358 }
359 encoded
360}
361
362/// Whether this request must carry a body.
363///
364/// Insert, update and upsert all write values that can only come from one.
365/// A delete names its rows in the query string, and a function call may take
366/// no arguments at all.
367fn needs_payload(api_request: &ApiRequest) -> bool {
368 use postrust_core::api_request::{Action, DbAction, Mutation};
369
370 matches!(
371 &api_request.action,
372 Action::Db(DbAction::RelationMut {
373 mutation: Mutation::Create | Mutation::Update | Mutation::SingleUpsert,
374 ..
375 })
376 )
377}
378
379/// Classify a JWT failure the way PostgREST reports it.
380///
381/// A token that could not be read at all and one that was read and found
382/// wanting are different answers to the client: the first says the credential
383/// is unusable, the second says what about it was unacceptable. PostgREST
384/// gives them different codes, and clients branch on them.
385fn jwt_error(error: postrust_auth::JwtError) -> postrust_core::Error {
386 use postrust_auth::JwtError;
387
388 match error {
389 JwtError::NoIdentity => postrust_core::Error::MissingAuth,
390 JwtError::SecretMissing => postrust_core::Error::JwtSecretMissing,
391 // Read, and found wanting: the token itself is intact, and what it
392 // claims is what was not accepted.
393 JwtError::Claim(fault) => postrust_core::Error::JwtClaim(fault.to_string()),
394 JwtError::InvalidSignature => {
395 postrust_core::Error::InvalidJwt("JWT cryptographic operation failed".into())
396 }
397 JwtError::InvalidHeaderFormat => {
398 postrust_core::Error::InvalidJwt("Unsupported token type".into())
399 }
400 JwtError::NoSuitableKey => postrust_core::Error::NoSuitableJwtKey,
401 }
402}
403
404/// Execute an action plan.
405async fn execute_plan(
406 state: &AppState,
407 api_request: &ApiRequest,
408 plan: &ActionPlan,
409 auth: &postrust_auth::AuthResult,
410 added_join_columns: &[String],
411) -> Result<QueryResult, postrust_core::Error> {
412 match plan {
413 ActionPlan::Db(db_plan) => {
414 // A media type the schema renders itself replaces the whole
415 // response body: the aggregate is applied over the rows the
416 // request selected, so it sees exactly what would otherwise have
417 // been serialised as JSON.
418 let media_handler = {
419 let schema_cache = state.schema_cache().await;
420 read_target(api_request, Some(db_plan), &schema_cache).and_then(|qi| {
421 api_request.accept_media_types.iter().find_map(|media| {
422 schema_cache
423 .media_handler(
424 &api_request.schema,
425 media.content_type(),
426 &qi,
427 has_default_select(&api_request.query_params.select),
428 )
429 .map(|(handler, resolved)| {
430 (
431 resolved.to_string(),
432 format!(
433 "{}.{}",
434 postrust_sql::escape_ident(&handler.aggregate.schema),
435 postrust_sql::escape_ident(&handler.aggregate.name)
436 ),
437 // Applied to the table's own row, not to
438 // the derived one, whenever the derived
439 // row would be a worse description of it.
440 // A handler naming a table needs the row
441 // type by name; one taking `anyelement`
442 // needs the columns to still *be* what
443 // they are -- a handler calling
444 // `ST_AsGeoJSON` cannot find a geometry in
445 // a row whose geometry this side already
446 // rendered to jsonb. Only where the
447 // request did not reshape the rows, since
448 // then the two are the same rows.
449 handler.table.is_some()
450 || has_default_select(&api_request.query_params.select),
451 handler.base_type.clone(),
452 )
453 })
454 })
455 })
456 };
457
458 // Content negotiation. A media type is rendered by a builtin, by a
459 // handler the schema declared, or by the function's own return
460 // type -- and a request that names only types nothing here can
461 // render has not been answered by serving it JSON under a
462 // `Content-Type` it never asked for. PostgREST refuses it, naming
463 // everything that was asked for.
464 if media_handler.is_none()
465 && declared_media_type(db_plan, api_request).is_none()
466 && !api_request
467 .accept_media_types
468 .iter()
469 .any(renders_without_a_handler)
470 {
471 return Err(postrust_core::Error::InvalidMediaType(
472 api_request
473 .accept_media_types
474 .iter()
475 .map(postrust_core::api_request::MediaType::to_mime)
476 .collect::<Vec<_>>()
477 .join(", "),
478 ));
479 }
480
481 // Carry the parent's whole row out of the inner query when a
482 // computed relationship needs it as an argument. It has to be
483 // taken here, where the real table is still in scope: a bare
484 // reference to the table yields its composite type, whereas the
485 // derived table it becomes one level up yields a `record`.
486 let needs_parent_row = added_join_columns.iter().any(|c| c == PARENT_ROW_COLUMN)
487 || matches!(&media_handler, Some((_, _, true, _)));
488 let db_plan = &if needs_parent_row {
489 let mut adjusted = db_plan.clone();
490 // A function's result is a relation too, so the row is
491 // reachable there by exactly the same means -- which is what
492 // makes a computed relationship work on
493 // `/rpc/getallvideogames` and not only on a table.
494 //
495 // Named after the table it returns, not after the function.
496 // The builder aliases the call to that table (`FROM
497 // test.getallvideogames() AS videogames`), because a computed
498 // *column* has to be able to name the row it is a function of.
499 // Naming the function here instead asks for a relation that is
500 // no longer in scope, and the request that used to work --
501 // `?select=name,designer:computed_designers(name)` -- answers
502 // `column "getallvideogames" does not exist`.
503 let (tree, relation) = match &mut adjusted {
504 DbActionPlan::Read(tree) => {
505 let relation = tree.root.from.name.clone();
506 (Some(tree), relation)
507 }
508 DbActionPlan::Call { read, .. } => {
509 let relation = read
510 .as_ref()
511 .map(|tree| tree.root.from.name.clone())
512 .unwrap_or_default();
513 (read.as_mut(), relation)
514 }
515 // The row is taken inside the statement, by `RETURNING`,
516 // and read back off the CTE as an ordinary column -- so
517 // unlike the two above there is no relation to name here,
518 // and the read tree selects the column rather than a row.
519 DbActionPlan::MutateRead { mutate, read } => {
520 let returning = match mutate {
521 postrust_core::plan::MutatePlan::Insert { returning, .. }
522 | postrust_core::plan::MutatePlan::Update { returning, .. }
523 | postrust_core::plan::MutatePlan::Delete { returning, .. } => {
524 returning
525 }
526 };
527 if !returning.iter().any(|c| c == PARENT_ROW_COLUMN) {
528 returning.push(PARENT_ROW_COLUMN.to_string());
529 }
530 // A mutation that returns no body has no read tree,
531 // and nothing to embed into either.
532 if let Some(read) = read.as_mut() {
533 if !read.root.select.iter().any(|field| {
534 field.alias.as_deref() == Some(PARENT_ROW_COLUMN)
535 || field.field.name == PARENT_ROW_COLUMN
536 }) {
537 read.root.select.push(
538 postrust_core::plan::CoercibleSelectField::simple(
539 PARENT_ROW_COLUMN,
540 "",
541 ),
542 );
543 }
544 }
545 (None, String::new())
546 }
547 };
548
549 if let Some(tree) = tree {
550 // An empty select means every column; naming the row would
551 // otherwise replace them rather than join them.
552 if tree.root.select.is_empty() {
553 tree.root
554 .select
555 .push(postrust_core::plan::CoercibleSelectField::simple("*", ""));
556 }
557
558 let mut field =
559 postrust_core::plan::CoercibleField::simple(relation, String::new());
560 field.full_row = true;
561 tree.root
562 .select
563 .push(postrust_core::plan::CoercibleSelectField {
564 field,
565 aggregate: None,
566 aggregate_cast: None,
567 cast: None,
568 alias: Some(PARENT_ROW_COLUMN.to_string()),
569 });
570 }
571 adjusted
572 } else {
573 db_plan.clone()
574 };
575
576 // The `Location` of a created row is built from its primary key,
577 // which the client may not have selected. The columns are added
578 // under names of our own so they cannot collide with anything the
579 // request asked for, and are taken back out of the response once
580 // the header is built.
581 //
582 // Only for `Prefer: return=headers-only`, which is the request
583 // that has no other way to learn where the row went. A client
584 // given the row back can read the key out of it, and PostgREST
585 // sends no `Location` there -- so neither does this. Deciding it
586 // here also means the key columns are never added to a select
587 // that had no use for them.
588 let location_keys: Vec<String> = match db_plan {
589 DbActionPlan::MutateRead {
590 mutate: postrust_core::plan::MutatePlan::Insert { pk_cols, .. },
591 ..
592 } if !is_upsert(api_request) && wants_location(api_request) => pk_cols.clone(),
593 _ => Vec::new(),
594 };
595 // Whether the write created a row or merged into an existing one.
596 // The statement is the only place that can say, so the answer is
597 // carried out of it as a column and taken back out of the response.
598 let reports_inserted = matches!(
599 db_plan,
600 DbActionPlan::MutateRead {
601 mutate: postrust_core::plan::MutatePlan::Insert {
602 on_conflict: Some(_),
603 reports_inserted: true,
604 ..
605 },
606 ..
607 }
608 );
609
610 let db_plan = &if location_keys.is_empty() && !reports_inserted {
611 db_plan.clone()
612 } else {
613 let mut adjusted = db_plan.clone();
614 if let DbActionPlan::MutateRead {
615 read: Some(tree), ..
616 } = &mut adjusted
617 {
618 for key in &location_keys {
619 tree.root.select.push(
620 postrust_core::plan::CoercibleSelectField::with_alias(
621 key,
622 "",
623 &format!("{}{}", LOCATION_KEY_PREFIX, key),
624 ),
625 );
626 }
627 if reports_inserted {
628 tree.root
629 .select
630 .push(postrust_core::plan::CoercibleSelectField::simple(
631 postrust_core::query::INSERTED_COLUMN,
632 "boolean",
633 ));
634 }
635 }
636 adjusted
637 };
638
639 // Build SQL
640 let query = postrust_core::query::build_query(
641 &ActionPlan::Db(db_plan.clone()),
642 Some(&auth.role),
643 )?;
644
645 if !query.has_main() {
646 return Ok(QueryResult::default());
647 }
648
649 let (mut sql, params) = query.build_main();
650
651 // A `Prefer: count` needs the same query without its page: the
652 // total is what the filters match, not what this page returned.
653 // LIMIT and OFFSET render as literals, so dropping them leaves the
654 // placeholders and their numbering untouched and the count query
655 // can be bound with the very same parameters.
656 let mut count_sql = match api_request.preferences.count {
657 Some(_) => unpaged_sql(db_plan, &auth.role)?,
658 None => None,
659 };
660
661 // Embed related resources in the same query.
662 //
663 // Each relation becomes a correlated subselect in the SELECT list,
664 // so PostgreSQL builds the nested JSON while it already has the
665 // parent row -- one round trip for the whole tree instead of one per
666 // relation per level, and no grouping or attaching in this process.
667 //
668 // Parent columns stay ordinary typed columns, so they are converted
669 // to JSON by the same code as a request without embeds and a NUMERIC
670 // or timestamp renders identically either way.
671 let mut embed_filters = EmbedFilters {
672 filters: &api_request.query_params.filters,
673 orders: &api_request.query_params.order,
674 ranges: &api_request.query_params.ranges,
675 logic: &api_request.query_params.logic,
676 params: Vec::new(),
677 base: params.len(),
678 max_rows: api_request.max_rows,
679 alias_counter: 0,
680 // A schema-declared handler produces the whole body from
681 // these rows, so each column reaches the client as *text* --
682 // and that is the one place the cast is observable. Keyed on
683 // whether there is a handler at all, not on which row shape it
684 // wants: `{"id":1}` against PostgREST's `{"id": 1}` is the
685 // difference either way.
686 rendering: match &media_handler {
687 Some(_) => postrust_core::EmbedRendering::Normalised,
688 None => postrust_core::EmbedRendering::Verbatim,
689 },
690 };
691
692 // The single-query form cannot express a spread, whose columns have
693 // to land in the parent object rather than under a key of their
694 // own. One anywhere in the tree sends the whole request down the
695 // two-query path, which can: taking the single-query path for the
696 // plain relations would embed those and drop the spread, since the
697 // two-query path only runs when the first produced nothing.
698 let embed_parent = {
699 let schema_cache = state.schema_cache().await;
700 read_target(api_request, Some(db_plan), &schema_cache)
701 };
702 let embed_level = match embed_parent.clone() {
703 Some(parent_qi)
704 if api_request.query_params.select.iter().any(|item| {
705 matches!(
706 item,
707 postrust_core::api_request::SelectItem::Relation { .. }
708 | postrust_core::api_request::SelectItem::SpreadRelation { .. }
709 )
710 }) =>
711 {
712 let schema_cache = state.schema_cache().await;
713 // A computed relationship takes the parent's row, which
714 // now reaches here on every path: a read names the table,
715 // and a mutation returns the row out of the statement
716 // where it still has the table's type rather than the
717 // CTE's anonymous one.
718 let parent_row = PARENT_ROW_COLUMN_REF;
719 build_embed_expressions(
720 &schema_cache,
721 &parent_qi,
722 "src",
723 parent_row,
724 &api_request.query_params.select,
725 &mut embed_filters,
726 &[],
727 &[],
728 )?
729 }
730 _ => EmbedLevel::default(),
731 };
732 let mut embed_level = embed_level;
733
734 // Ordering by an embedded column is resolved here rather than in
735 // the read plan: the plan has no way to reach another table.
736 if let (Some(parent_qi), DbActionPlan::Read(tree)) = (&embed_parent, db_plan) {
737 let schema_cache = state.schema_cache().await;
738 embed_level.orders = build_embed_orders(
739 &schema_cache,
740 parent_qi,
741 &tree.root.order,
742 &api_request.query_params.select,
743 &mut embed_filters,
744 )?;
745 }
746
747 // Filter parameters are appended in the order the predicates were
748 // renumbered, so placeholder N in the wrapped SQL lines up with
749 // params[N - 1].
750 let mut params = params;
751 params.extend(embed_filters.params);
752
753 // Built whenever there were relations at all, not only when they
754 // produced expressions: `clients!inner()` contributes no column
755 // but does narrow the parent, and its predicate binds parameters
756 // that have already been appended -- left unapplied, the query is
757 // sent with more parameters than it uses.
758 if embed_level.saw_relations {
759 // `!inner` removes parent rows, so it has to be applied before
760 // the page is taken. Left where it is, the inner query's
761 // LIMIT would run first and the join would then trim an
762 // already-truncated page -- a request for one row could come
763 // back empty. So the inner query is rebuilt unpaged and the
764 // range moves out to the wrapper, after the join.
765 //
766 // Rebuilding is safe for parameter numbering: LIMIT and OFFSET
767 // render as literals, so dropping them leaves the placeholders
768 // and their count untouched.
769 //
770 // Ordering by an embedded resource's column needs the same
771 // treatment for the same reason: the value being ordered on is
772 // only reachable out here, so the page cannot be taken before
773 // it is known.
774 let orders_by_embed = matches!(db_plan, DbActionPlan::Read(tree)
775 if tree.root.order.iter().any(|t| t.relation.is_some()));
776
777 let mut page = None;
778 if !embed_level.inner_joins.is_empty() || orders_by_embed {
779 if let DbActionPlan::Read(tree) = db_plan {
780 let mut unpaged = tree.clone();
781 page = Some(std::mem::take(&mut unpaged.root.range));
782 sql = postrust_core::query::build_query(
783 &ActionPlan::Db(DbActionPlan::Read(unpaged)),
784 Some(&auth.role),
785 )?
786 .build_main()
787 .0;
788 }
789 }
790
791 let mut projection = String::from("src.*");
792 for (field_name, expression) in &embed_level.expressions {
793 projection.push_str(", ");
794 projection.push_str(expression);
795 projection.push_str(" AS ");
796 projection.push_str(&postrust_sql::escape_ident(field_name));
797 }
798 // A data-modifying `WITH` has to stay at the top level:
799 // PostgreSQL refuses one nested inside a subquery, which is
800 // where wrapping the whole statement would put it. The clause
801 // is lifted out and put back in front of the wrapper.
802 let inner_sql = std::mem::take(&mut sql);
803 let (with_clause, inner_sql) = split_leading_cte(&inner_sql);
804 sql = format!(
805 "{}SELECT {} FROM ({}) AS src",
806 with_clause, projection, inner_sql
807 );
808
809 // `?clients=is.null` asks whether the embed matched anything,
810 // not about a column. The embed's expression is the thing to
811 // test, and it is only in scope out here -- referring to the
812 // name it is given would be referring to a column of the very
813 // select that defines it.
814 let mut predicates = embed_level.inner_joins.clone();
815 for filter in &api_request.query_params.filters_root {
816 let Some((_, exists)) = embed_level
817 .filterable
818 .iter()
819 .find(|(name, _)| name == &filter.field.name)
820 else {
821 continue;
822 };
823 let negated = match &filter.op_expr.operation {
824 postrust_core::api_request::Operation::Is(
825 postrust_core::api_request::IsValue::Null,
826 ) => filter.op_expr.negated,
827 postrust_core::api_request::Operation::Is(
828 postrust_core::api_request::IsValue::NotNull,
829 ) => !filter.op_expr.negated,
830 _ => continue,
831 };
832 // `is.null` asks for parents the embed did not match, so
833 // the existence test is the negation of it.
834 predicates.push(match negated {
835 true => exists.clone(),
836 false => format!("NOT {}", exists),
837 });
838 }
839
840 // `or=(clientinfo.not.is.null,contact.not.is.null)` asks the
841 // same question of several embeds at once, and the answer is
842 // one predicate combining their existence tests. The read
843 // plan left the tree alone precisely so that it could be
844 // built here, where the embeds are in scope.
845 let embeds =
846 postrust_core::api_request::embedded_names(&api_request.query_params.select);
847 for (path, tree) in &api_request.query_params.logic {
848 if !path.is_empty() || !tree.names_only(&embeds) {
849 continue;
850 }
851 if let Some(predicate) = embed_logic_predicate(tree, &embed_level.filterable) {
852 predicates.push(predicate);
853 }
854 }
855
856 if !predicates.is_empty() {
857 sql.push_str(" WHERE ");
858 sql.push_str(&predicates.join(" AND "));
859 }
860
861 if !embed_level.orders.is_empty() {
862 sql.push_str(" ORDER BY ");
863 sql.push_str(&embed_level.orders.join(", "));
864 }
865
866 // `!inner` and `?rel=is.null` decide which parent rows survive,
867 // so the count has to be taken after them -- the same wrapper,
868 // over the unpaged query.
869 count_sql = count_sql.map(|base| {
870 let (with_clause, base) = split_leading_cte(&base);
871 let mut counted = format!(
872 "{}SELECT {} FROM ({}) AS src",
873 with_clause, projection, base
874 );
875 if !predicates.is_empty() {
876 counted.push_str(" WHERE ");
877 counted.push_str(&predicates.join(" AND "));
878 }
879 counted
880 });
881
882 if let Some(range) = page {
883 if let Some(limit) = range.limit {
884 sql.push_str(&format!(" LIMIT {}", limit));
885 }
886 if range.offset > 0 {
887 sql.push_str(&format!(" OFFSET {}", range.offset));
888 }
889 }
890 }
891
892 // `application/geo+json` has a rendering of its own even where no
893 // schema declares one: a FeatureCollection whose geometry is the
894 // table's geometry column and whose properties are the rest. A
895 // schema-declared handler overrides it, which is why this only
896 // runs when none matched.
897 //
898 // Asking for GeoJSON from rows that carry no geometry is an error
899 // rather than a plain JSON body: the client asked for features,
900 // and a list of objects with no geometry is not features.
901 let wants_geojson = media_handler.is_none()
902 && api_request
903 .accept_media_types
904 .iter()
905 .any(|m| matches!(m, postrust_core::api_request::MediaType::GeoJson));
906 let geojson_column = if wants_geojson {
907 let schema_cache = state.schema_cache().await;
908 let target = read_target(api_request, Some(db_plan), &schema_cache);
909 match target.as_ref().and_then(|qi| schema_cache.get_table(qi)) {
910 Some(table) => match geometry_key(table, &api_request.query_params.select) {
911 Some(column) => Some(column),
912 None => return Err(postrust_core::Error::MissingGeometryColumn),
913 },
914 // Nothing this side can name a geometry column on -- a
915 // scalar function, say. Left to the database, which is
916 // where the answer was coming from anyway.
917 None => None,
918 }
919 } else {
920 None
921 };
922
923 if let Some((media_type, aggregate, over_row, _)) = &media_handler {
924 // The aggregate takes the table's own row type. A derived
925 // table yields `record`, which will not cast to it, so where
926 // the handler names a table the aggregate is applied to the
927 // column carrying the real row -- the same column a computed
928 // relationship reads. A handler taking `anyelement` has no
929 // such requirement and takes the derived row directly.
930 let argument = if *over_row {
931 format!(
932 "pgrst_media.{}",
933 postrust_sql::escape_ident(PARENT_ROW_COLUMN)
934 )
935 } else {
936 "pgrst_media".to_string()
937 };
938 // `count(*)` alongside the handler: both are aggregates over
939 // the same set, and the rendered body is one value that has
940 // forgotten how many rows went into it. `Content-Range`
941 // describes the row set rather than the rendering, so the
942 // count has to be carried out of the same statement -- asking
943 // again afterwards would be a second snapshot.
944 let (with_clause, inner) = split_leading_cte(&sql);
945 sql = format!(
946 "{}SELECT {}({})::text AS pgrst_body, count(*) AS {} \
947 FROM ({}) pgrst_media",
948 with_clause, aggregate, argument, MEDIA_ROW_COUNT_COLUMN, inner
949 );
950 debug!("Rendering {} via {}", media_type, aggregate);
951 }
952
953 debug!("Executing SQL: {}", sql);
954 debug!("With {} parameters", params.len());
955
956 // Everything for this request runs in one transaction.
957 //
958 // `SET LOCAL ROLE` and `set_config(..., true)` are scoped to a
959 // transaction. Sent on a bare pooled connection they apply to their
960 // own implicit single-statement transaction and are gone before the
961 // next statement runs, so the query executed as the pool's login
962 // role with no JWT claims set -- row-level security and role grants
963 // were not being applied at all. PostgreSQL says so in its log:
964 // "SET LOCAL can only be used in transaction blocks".
965 //
966 // Embedding runs on this same transaction, which also means a
967 // parent row and its children come from one snapshot rather than
968 // from two separate reads.
969 let mut tx = state
970 .pool
971 .begin()
972 .await
973 .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
974
975 // Isolation level. `SET TRANSACTION` is only accepted before the
976 // first statement of a transaction, so it goes here.
977 //
978 // A role's own setting wins over the server-wide
979 // `PGRST_DB_TX_ISOLATION`, and both are closed enums rendered by
980 // `to_sql` -- never caller text. Issued only when it differs from
981 // PostgreSQL's own default, so the common case costs no round trip.
982 let isolation = state
983 .config
984 .role_settings
985 .get(&auth.role)
986 .and_then(|r| r.isolation_level.as_ref())
987 .unwrap_or(&state.config.db_tx_isolation);
988 if *isolation != postrust_core::IsolationLevel::ReadCommitted {
989 sqlx::query(&format!(
990 "SET TRANSACTION ISOLATION LEVEL {}",
991 isolation.to_sql()
992 ))
993 .execute(&mut *tx)
994 .await
995 .map_err(map_sqlx_error)?;
996 }
997
998 // A read request runs in a read-only transaction.
999 //
1000 // Without this a GET can change data: `GET /rpc/some_volatile_fn`
1001 // happily executes whatever the function does. PostgreSQL is the
1002 // right place to enforce it, since it covers anything reachable
1003 // from the statement -- a volatile function, a trigger, a rule --
1004 // rather than only what the planner thought to look at.
1005 if is_read_only(api_request) {
1006 sqlx::query("SET TRANSACTION READ ONLY")
1007 .execute(&mut *tx)
1008 .await
1009 .map_err(map_sqlx_error)?;
1010 }
1011
1012 // Everything the request tells the database about itself, in
1013 // one statement.
1014 //
1015 // These are `SET LOCAL` by another spelling -- `set_config(k, v,
1016 // true)` -- and a function or an RLS policy reads them back with
1017 // `current_setting`. Sent as one `SELECT` rather than one
1018 // statement each, which is a round trip per setting otherwise, and
1019 // there are eight of them before a request has done any work.
1020 //
1021 // The order is PostgREST's: the settings a role carries, then the
1022 // role, then everything about the request. The role goes on after
1023 // its own settings so that a `GRANT SET ON PARAMETER` on the
1024 // authenticator still applies when they are set.
1025 {
1026 let mut path = vec![api_request.schema.clone()];
1027 path.extend(state.config.db_extra_search_path.iter().cloned());
1028 let search_path = path
1029 .iter()
1030 .map(|s| postrust_sql::escape_ident(s))
1031 .collect::<Vec<_>>()
1032 .join(", ");
1033
1034 // The claims carry the role, as PostgREST's do: a policy
1035 // reading `request.jwt.claims` finds the role it is running as
1036 // whether or not the token named one.
1037 let mut claims: serde_json::Map<String, serde_json::Value> = auth
1038 .claims
1039 .iter()
1040 .map(|(k, v)| (k.clone(), v.clone()))
1041 .collect();
1042 claims.insert(
1043 "role".to_string(),
1044 serde_json::Value::String(auth.role.clone()),
1045 );
1046
1047 let mut settings: Vec<(String, String)> = Vec::new();
1048
1049 // The settings a role carries come first, before the `role`
1050 // that follows them -- see the note above on
1051 // `GRANT SET ON PARAMETER`.
1052 if let Some(role_settings) = state.config.role_settings.get(&auth.role) {
1053 if let Some(timeout) = role_settings.statement_timeout {
1054 settings.push(("statement_timeout".to_string(), timeout.to_string()));
1055 }
1056 }
1057
1058 settings.extend([
1059 ("search_path".to_string(), search_path),
1060 ("role".to_string(), auth.role.clone()),
1061 (
1062 "request.jwt.claims".to_string(),
1063 serde_json::Value::Object(claims).to_string(),
1064 ),
1065 ("request.method".to_string(), api_request.method.clone()),
1066 ("request.path".to_string(), api_request.path.clone()),
1067 (
1068 "request.headers".to_string(),
1069 json_object(&api_request.headers),
1070 ),
1071 (
1072 "request.cookies".to_string(),
1073 json_object(&api_request.cookies),
1074 ),
1075 ]);
1076
1077 // `app_settings`, exposed as PostgREST spells them: a function
1078 // or policy reads one back with
1079 // `current_setting('app.settings.<key>')`.
1080 for (key, value) in &state.config.app_settings {
1081 settings.push((format!("app.settings.{key}"), value.clone()));
1082 }
1083
1084 // `Prefer: timezone` is applied to the session, so every value
1085 // the database renders -- and every `now()` a function calls --
1086 // sees it. An unknown zone is PostgreSQL's to reject, and it
1087 // does, with a message naming the value the client sent.
1088 if let Some(timezone) = &api_request.preferences.timezone {
1089 settings.push(("timezone".to_string(), timezone.clone()));
1090 }
1091
1092 let calls = (0..settings.len())
1093 .map(|i| format!("set_config(${}, ${}, true)", i * 2 + 1, i * 2 + 2))
1094 .collect::<Vec<_>>()
1095 .join(", ");
1096 let sql = format!("SELECT {}", calls);
1097 let mut query = sqlx::query(&sql);
1098 for (name, value) in &settings {
1099 query = query.bind(name).bind(value);
1100 }
1101 query.execute(&mut *tx).await.map_err(map_sqlx_error)?;
1102 }
1103
1104 // `db_pre_request`: a function called before the request's own
1105 // query, on the same transaction, so it sees every setting above
1106 // and anything it raises aborts the request. PostgREST's use for
1107 // it is a custom authorisation check that `RAISE`s to refuse.
1108 //
1109 // The name is an identifier from the configuration, not from the
1110 // request, and is escaped as one -- a schema-qualified name stays
1111 // two identifiers so `auth.check` does not become `"auth.check"`.
1112 if let Some(pre_request) = &state.config.db_pre_request {
1113 let qualified = pre_request
1114 .split('.')
1115 .map(postrust_sql::escape_ident)
1116 .collect::<Vec<_>>()
1117 .join(".");
1118 sqlx::query(&format!("SELECT {qualified}()"))
1119 .execute(&mut *tx)
1120 .await
1121 .map_err(map_sqlx_error)?;
1122 }
1123
1124 // Execute main query with bound parameters
1125 let rows = bind_params(sqlx::query(&sql), ¶ms)
1126 .fetch_all(&mut *tx)
1127 .await
1128 .map_err(|e| {
1129 error!("Query error: {}", e);
1130 map_sqlx_error(e)
1131 })?;
1132
1133 // Convert rows to JSON.
1134 //
1135 // `into_iter` matters for large result sets: each row's buffers are
1136 // freed as soon as it has been converted, rather than the whole
1137 // `Vec<PgRow>` staying alive alongside the whole `Vec<Value>`.
1138 // A rendered media type is a single value, not a row set: the
1139 // aggregate already produced the entire body.
1140 if let Some((media_type, base_type)) = media_handler
1141 .as_ref()
1142 .map(|(m, _, _, base)| (m.clone(), base.clone()))
1143 .or_else(|| declared_media_type(db_plan, api_request))
1144 {
1145 // The value's type is a domain this side has no mapping for,
1146 // so it arrives as PostgreSQL's text rendering of it. For
1147 // anything text-like that rendering *is* the value; for a
1148 // `bytea` it is `\x` and hex, which describes the bytes
1149 // rather than being them.
1150 let mut body = rows
1151 .first()
1152 .and_then(|row| raw_column(row, &base_type))
1153 .unwrap_or_default();
1154
1155 // PostgREST reads a handler's value as raw bytes in binary
1156 // format, and a `jsonb` in binary format begins with a version
1157 // byte -- which it forwards to the client. Its own suite
1158 // records the result and marks it: "TODO SOH (start of
1159 // heading) is being added to results".
1160 //
1161 // It is a bug, and the byte makes the body invalid JSON, so it
1162 // is not something to do by default. Compatibility mode is
1163 // where this server sets out to be PostgREST rather than to be
1164 // right, and a client written against PostgREST's bytes gets
1165 // PostgREST's bytes.
1166 if state.config.compat_mode && base_type == "jsonb" {
1167 body.insert(0, 0x01);
1168 }
1169
1170 // A rendered body is still a window on a result set, and
1171 // PostgREST reports one. The row count comes from the
1172 // aggregate's own statement where a handler rendered the rows;
1173 // where the value is a function's media-type domain there was
1174 // no aggregate, and the rows in hand are the answer.
1175 let rendered_rows = match media_handler.is_some() {
1176 true => rows
1177 .first()
1178 .and_then(|row| {
1179 sqlx::Row::try_get::<i64, _>(row, MEDIA_ROW_COUNT_COLUMN).ok()
1180 })
1181 .unwrap_or_default(),
1182 false => rows.len() as i64,
1183 };
1184 let total = match (&api_request.preferences.count, &count_sql) {
1185 (Some(preference), Some(count_sql)) => {
1186 resolve_count(
1187 &mut tx,
1188 preference,
1189 count_sql,
1190 ¶ms,
1191 api_request.max_rows,
1192 )
1193 .await?
1194 }
1195 _ => None,
1196 };
1197 let content_range = postrust_response::ContentRange::from_pagination(
1198 top_level_offset(api_request),
1199 rendered_rows,
1200 total,
1201 );
1202
1203 tx.commit()
1204 .await
1205 .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
1206 return Ok(QueryResult {
1207 status: StatusCode::OK,
1208 raw_body: Some((media_type, body)),
1209 content_range: Some(content_range),
1210 ..QueryResult::default()
1211 });
1212 }
1213
1214 let mut json_rows: Vec<serde_json::Value> = rows
1215 .into_iter()
1216 .map(|row| postrust_core::row_json::row_to_json(&row))
1217 .collect();
1218
1219 // Embeds already came back with the parent query when the SELECT
1220 // list carried relations. This path remains for anything the
1221 // single-query form did not handle.
1222 let two_query_parent = {
1223 let schema_cache = state.schema_cache().await;
1224 read_target(api_request, Some(db_plan), &schema_cache)
1225 };
1226 if let Some(parent_qi) = two_query_parent.filter(|_| !embed_level.saw_relations) {
1227 let schema_cache = state.schema_cache().await;
1228 embed_relations(
1229 &mut tx,
1230 &schema_cache,
1231 &parent_qi,
1232 &api_request.query_params.select,
1233 &mut json_rows,
1234 api_request.max_rows,
1235 )
1236 .await?;
1237 }
1238
1239 // A function can set the response's status and headers, and only
1240 // it knows whether it did -- so the settings are read back after
1241 // it has run, on the same transaction. Reads cannot set them
1242 // without a function of their own having run, which is what a
1243 // call or a mutation is.
1244 let (guc_headers, guc_status) =
1245 match is_mutation(db_plan) || matches!(db_plan, DbActionPlan::Call { .. }) {
1246 false => (None, None),
1247 true => {
1248 use sqlx::Row;
1249 let row = sqlx::query(
1250 "SELECT current_setting('response.headers', true), \
1251 current_setting('response.status', true)",
1252 )
1253 .fetch_one(&mut *tx)
1254 .await
1255 .map_err(map_sqlx_error)?;
1256 // Empty is absent. Once any transaction on this
1257 // connection has defined a custom setting, PostgreSQL
1258 // keeps the name for the rest of the session and
1259 // reports it as `''` rather than null -- so a request
1260 // that set nothing would otherwise inherit "the empty
1261 // headers" from whichever request last set some.
1262 let setting = |index: usize| {
1263 row.try_get::<Option<String>, _>(index)
1264 .ok()
1265 .flatten()
1266 .filter(|value| !value.trim().is_empty())
1267 };
1268 (setting(0), setting(1))
1269 }
1270 };
1271
1272 // Both are the function's own words, so both are checked before
1273 // they reach the response: a malformed one is a fault in the
1274 // schema, and saying so is more use than a header the client
1275 // cannot see or a status it cannot explain.
1276 if let Some(headers) = &guc_headers {
1277 if postrust_response::parse_guc_headers(headers).is_none() {
1278 return Err(postrust_core::Error::InvalidGucHeaders);
1279 }
1280 }
1281 let guc_status = match guc_status {
1282 None => None,
1283 Some(status) => Some(
1284 status
1285 .trim()
1286 .parse::<u16>()
1287 .ok()
1288 .and_then(|code| StatusCode::from_u16(code).ok())
1289 .ok_or(postrust_core::Error::InvalidGucStatus)?,
1290 ),
1291 };
1292
1293 // The count runs on the same transaction and so under the same
1294 // snapshot as the page it describes.
1295 let total = match (&api_request.preferences.count, &count_sql) {
1296 (Some(preference), Some(count_sql)) => {
1297 resolve_count(
1298 &mut tx,
1299 preference,
1300 count_sql,
1301 ¶ms,
1302 api_request.max_rows,
1303 )
1304 .await?
1305 }
1306 _ => None,
1307 };
1308
1309 // Reads take no locks worth holding and write plans commit their
1310 // work, so the transaction is committed either way.
1311 tx.commit()
1312 .await
1313 .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
1314
1315 // Drop columns that were only selected to join the embeds.
1316 //
1317 // Except where the embed answers to that very name: embedding
1318 // through a column -- `?select=client_id(*)` -- keys the result by
1319 // the column it joined on, and the embed is what the client asked
1320 // for. Stripping by name would take it along with the column.
1321 let embed_keys: std::collections::HashSet<&str> = embed_level
1322 .expressions
1323 .iter()
1324 .map(|(key, _)| key.as_str())
1325 .collect();
1326
1327 for column in added_join_columns
1328 .iter()
1329 .filter(|column| !embed_keys.contains(column.as_str()))
1330 {
1331 for row in json_rows.iter_mut() {
1332 if let Some(object) = row.as_object_mut() {
1333 object.remove(column);
1334 }
1335 }
1336 }
1337
1338 // A spread's columns belong to the object above it. This happens
1339 // after the join columns are dropped, not before: a spread may
1340 // legitimately produce a column of the same name as one added for
1341 // joining, and dropping it afterwards would take the wrong one.
1342 let spread_columns: std::collections::HashMap<String, Vec<String>> =
1343 embed_level.spread_columns.iter().cloned().collect();
1344 for row in json_rows.iter_mut() {
1345 flatten_spreads(row, &spread_columns);
1346 }
1347
1348 // In PostgREST-compatibility mode, reshape RPC responses to match
1349 // PostgREST: un-nest the function-name-keyed column and return a
1350 // bare value for non-set-returning functions.
1351 let (mut json_rows, singular) = if state.config.compat_mode {
1352 if let ActionPlan::Db(DbActionPlan::Call { call, .. }) = plan {
1353 unwrap_rpc_rows(json_rows, call)
1354 } else {
1355 (json_rows, false)
1356 }
1357 } else {
1358 (json_rows, false)
1359 };
1360
1361 // A function returning `void` has no result to report. PostgREST
1362 // answers 204 rather than a body of `null`, which is the honest
1363 // shape for something that returns nothing.
1364 let returns_void = matches!(
1365 db_plan,
1366 DbActionPlan::Call { call, .. } if call.returns_void
1367 );
1368
1369 // `Prefer: max-affected` guards against a filter that turned out
1370 // to match more than the client meant. Nothing has been committed
1371 // yet, so refusing here undoes the whole statement.
1372 if let (Some(limit), postrust_core::api_request::PreferHandling::Strict) = (
1373 api_request.preferences.max_affected,
1374 &api_request.preferences.handling,
1375 ) {
1376 let affected = json_rows.len() as i64;
1377 let writes = is_mutation(db_plan) || matches!(db_plan, DbActionPlan::Call { .. });
1378 if writes && affected > limit {
1379 return Err(postrust_core::Error::MaxAffectedExceeded(affected));
1380 }
1381 }
1382
1383 // Any row this statement created rather than merged into. Read
1384 // before the column is taken back out of the response.
1385 let created_a_row = reports_inserted
1386 && json_rows.iter().any(|row| {
1387 row.get(postrust_core::query::INSERTED_COLUMN)
1388 == Some(&serde_json::Value::Bool(true))
1389 });
1390 if reports_inserted {
1391 for row in json_rows.iter_mut() {
1392 if let Some(object) = row.as_object_mut() {
1393 object.remove(postrust_core::query::INSERTED_COLUMN);
1394 }
1395 }
1396 }
1397
1398 // A `PUT` writes the row its URL names, and exactly that row. The
1399 // filters were applied to the body, so a body naming some other
1400 // row wrote nothing -- and one naming several wrote several. Both
1401 // are the same mistake, and PostgreSQL cannot report it because
1402 // neither is a database error.
1403 if is_upsert(api_request) && json_rows.len() != 1 {
1404 return Err(postrust_core::Error::PutMatchingPk);
1405 }
1406
1407 // The created row's own address, taken from its key and then
1408 // removed from the body -- the client asked for a `?select=`, not
1409 // for the key.
1410 let location = build_location(api_request, &mut json_rows, &location_keys);
1411
1412 // A mutation returns the affected rows only when the caller asked
1413 // for them; otherwise the body is empty whatever the status.
1414 let omit_body = is_mutation(db_plan) && !wants_representation(api_request);
1415 // Taken before the rows are dropped: how many a write touched is
1416 // still the answer to `Content-Range` and to a request for a
1417 // single object.
1418 let affected = json_rows.len();
1419 let rows = if omit_body { Vec::new() } else { json_rows };
1420
1421 // `application/geo+json` is a rendering of these same rows, done
1422 // here rather than in SQL so that everything a mutation reports
1423 // has already happened to them: the status derived, the location
1424 // taken from the key, the bookkeeping columns removed. Built in
1425 // SQL it saw the row as the statement left it, and leaked
1426 // `pgrst_inserted` into a feature's properties.
1427 let geojson_body = geojson_column
1428 .as_ref()
1429 .filter(|_| !omit_body)
1430 .map(|column| {
1431 (
1432 "application/geo+json".to_string(),
1433 feature_collection(&rows, column),
1434 )
1435 });
1436
1437 // PostgREST reports the returned window on every successful data
1438 // response -- reads, mutations and RPC alike, but not on errors or
1439 // OPTIONS. Without an exact count the total is unknown, which
1440 // renders as `*` and keeps the status at 200.
1441 // The window starts where the request asked it to, and `offset`
1442 // overrides the `Range` header exactly as it does when the plan
1443 // resolves the range -- otherwise the reported window says 0 for a
1444 // request that plainly skipped rows.
1445 let offset = top_level_offset(api_request);
1446
1447 // What a mutation reports is not a window on a result set. An
1448 // insert or a delete reports no window at all -- `*` -- because
1449 // the rows it wrote are not a page of anything; an update reports
1450 // how many it changed; and an upsert reports nothing, the client
1451 // having named the row itself. PostgREST draws exactly these
1452 // lines, and a client reading the header expects them.
1453 use postrust_core::api_request::Mutation;
1454 let content_range = match mutation_kind(api_request) {
1455 Some(Mutation::SingleUpsert) => None,
1456 Some(Mutation::Create) | Some(Mutation::Delete) => {
1457 Some(postrust_response::ContentRange::new(1, 0, total))
1458 }
1459 Some(Mutation::Update) => Some(postrust_response::ContentRange::new(
1460 0,
1461 affected as i64 - 1,
1462 total,
1463 )),
1464 None => Some(postrust_response::ContentRange::from_pagination(
1465 offset,
1466 rows.len() as i64,
1467 total,
1468 )),
1469 };
1470
1471 // An offset past the end of the result is a range the server
1472 // cannot satisfy, and saying how many rows there actually are is
1473 // what lets the client correct it. Only reachable with a count
1474 // preference: without one the total is unknown and there is
1475 // nothing to be past the end of.
1476 if content_range
1477 .as_ref()
1478 .is_some_and(|range| range.status() == StatusCode::RANGE_NOT_SATISFIABLE)
1479 && mutation_kind(api_request).is_none()
1480 {
1481 return Err(postrust_core::Error::InvalidRange(format!(
1482 "An offset of {} was requested, but there are only {} rows.",
1483 offset,
1484 total.unwrap_or(0)
1485 )));
1486 }
1487
1488 Ok(QueryResult {
1489 // A status the function set outright wins: it knows things
1490 // about the outcome that the request's shape cannot say.
1491 status: match (guc_status, returns_void) {
1492 (Some(status), _) => status,
1493 (None, true) => StatusCode::NO_CONTENT,
1494 (None, false) => {
1495 mutation_status(db_plan, api_request, created_a_row, reports_inserted)
1496 .unwrap_or_else(|| {
1497 content_range
1498 .as_ref()
1499 .map(postrust_response::ContentRange::status)
1500 .unwrap_or(StatusCode::OK)
1501 })
1502 }
1503 },
1504 rows,
1505 singular,
1506 omit_body: omit_body || returns_void,
1507 affected,
1508 location,
1509 content_range,
1510 guc_headers,
1511 raw_body: geojson_body,
1512 ..Default::default()
1513 })
1514 }
1515 ActionPlan::Info(info_plan) => {
1516 use postrust_core::plan::InfoPlan;
1517
1518 // Return appropriate metadata based on the info type
1519 let response_data = match info_plan {
1520 InfoPlan::OpenApiSpec => {
1521 // Return basic server info for root endpoint
1522 serde_json::json!({
1523 "name": "postrust",
1524 "version": env!("CARGO_PKG_VERSION"),
1525 "description": "PostgREST-compatible REST API for PostgreSQL"
1526 })
1527 }
1528 InfoPlan::RelationInfo(qi) => {
1529 serde_json::json!({
1530 "schema": qi.schema,
1531 "name": qi.name,
1532 "type": "relation"
1533 })
1534 }
1535 InfoPlan::RoutineInfo(qi) => {
1536 serde_json::json!({
1537 "schema": qi.schema,
1538 "name": qi.name,
1539 "type": "routine"
1540 })
1541 }
1542 };
1543
1544 Ok(QueryResult {
1545 status: StatusCode::OK,
1546 rows: vec![response_data],
1547 ..Default::default()
1548 })
1549 }
1550 }
1551}
1552
1553/// Answer an `OPTIONS` with the methods its resource actually has.
1554///
1555/// This runs outside the CORS layer because that layer answers every `OPTIONS`
1556/// itself and never calls what it wraps -- `if parts.method == Method::OPTIONS`
1557/// returns immediately, preflight or not. So an `OPTIONS` never reached the
1558/// request handler at all: the body was empty because nothing built one, and
1559/// `Allow` was missing because the code that knows the answer was never asked.
1560///
1561/// The CORS layer still writes the response, and this adds to it, so a
1562/// preflight keeps every header it had.
1563pub async fn options_allow(
1564 State(state): State<Arc<AppState>>,
1565 request: Request,
1566 next: axum::middleware::Next,
1567) -> Response {
1568 if request.method() != http::Method::OPTIONS {
1569 return next.run(request).await;
1570 }
1571
1572 // Not every route is the API's. A layer added to the router wraps every
1573 // one of them, so `/admin` and `/_` arrive here too, and answering those
1574 // with what a table allows -- or with "no such table" -- takes a working
1575 // preflight and turns it into a 404.
1576 let Some(path) = api_path(request.uri().path(), state.config.compat_mode) else {
1577 return next.run(request).await;
1578 };
1579
1580 let verbatim_db_errors = state.config.compat_mode;
1581
1582 // Built here, from borrows, before anything is awaited: `axum::body::Body`
1583 // is `Send` but not `Sync`, and `&T` is `Send` only where `T` is `Sync`, so
1584 // a `&Request` held across an await makes the whole future not `Send` --
1585 // which a middleware has to be.
1586 let probe = probe_request(&request, path);
1587 let allow = match probe {
1588 Ok(probe) => resolve_allow(&state, probe).await,
1589 Err(error) => Err(error),
1590 };
1591
1592 let mut response = next.run(request).await;
1593
1594 match allow {
1595 Ok(allow) => {
1596 if let Ok(value) = http::HeaderValue::from_str(&allow) {
1597 response.headers_mut().insert(http::header::ALLOW, value);
1598 }
1599 response
1600 }
1601 // Asking what may be done with a table that is not there is answered
1602 // the same way as asking for the table.
1603 Err(error) => {
1604 let mut refusal = error_response(error, verbatim_db_errors).into_response();
1605 // The CORS layer wrote its headers on the response this replaces.
1606 // Without them a browser reports the request as blocked by CORS
1607 // rather than as the 404 it earned, which is a different thing to
1608 // go and fix.
1609 for (name, value) in response.headers() {
1610 if name.as_str().starts_with("access-control-") || name == http::header::VARY {
1611 refusal.headers_mut().append(name, value.clone());
1612 }
1613 }
1614 refusal
1615 }
1616 }
1617}
1618
1619/// The path the API parser should read, or `None` where the request names
1620/// something that is not the REST surface.
1621///
1622/// A layer added with `Router::layer` wraps each endpoint, and `nest` puts its
1623/// prefix-stripping *inside* that endpoint -- so the path seen here is the one
1624/// the client wrote and still carries the mount it arrived under, unlike the
1625/// one the handler reads.
1626fn api_path(path: &str, compat_mode: bool) -> Option<&str> {
1627 /// Whether a path names a mount point or something under it.
1628 fn under(path: &str, mount: &str) -> bool {
1629 path == mount || path.strip_prefix(mount).is_some_and(|r| r.starts_with('/'))
1630 }
1631
1632 // Every mount this server answers itself. In compatibility mode the REST
1633 // surface sits at the root, so a path not named here is read as a table:
1634 // `/healthz` becomes a question about a table called `healthz`, and the
1635 // two-segment ones are not a valid resource at all. Either way the
1636 // refusal replaces the preflight the CORS layer wrote, which is how a
1637 // browser comes to report a working endpoint as blocked.
1638 for mount in [
1639 "/admin",
1640 "/_",
1641 "/api/graphql",
1642 "/healthz",
1643 "/v1/version",
1644 "/v1/graphql",
1645 "/v1alpha1/graphql",
1646 ] {
1647 if under(path, mount) {
1648 return None;
1649 }
1650 }
1651 if under(path, "/api") {
1652 return Some(match path.strip_prefix("/api") {
1653 None | Some("") => "/",
1654 Some(rest) => rest,
1655 });
1656 }
1657 // Everywhere else the REST surface exists only in compatibility mode,
1658 // where it answers at the root. Without it `/` is this server's own
1659 // directory of endpoints, which allows what its handler allows.
1660 compat_mode.then_some(path)
1661}
1662
1663/// The request again, with an empty body and the API's own path, for the
1664/// parser to read.
1665fn probe_request(
1666 request: &Request,
1667 path: &str,
1668) -> Result<http::Request<bytes::Bytes>, postrust_core::Error> {
1669 let target = match request.uri().query() {
1670 Some(query) => format!("{path}?{query}"),
1671 None => path.to_string(),
1672 };
1673 let mut builder = http::Request::builder()
1674 .method(request.method().clone())
1675 .uri(target);
1676 for (name, value) in request.headers() {
1677 builder = builder.header(name, value);
1678 }
1679 builder
1680 .body(bytes::Bytes::new())
1681 .map_err(|e| postrust_core::Error::Internal(e.to_string()))
1682}
1683
1684/// What the resource this request names allows, or why it cannot say.
1685///
1686/// The path is resolved by the same parser the handler uses rather than by
1687/// splitting on `/` here, so a percent-encoded table name, an `Accept-Profile`
1688/// naming another schema, and the `/rpc/` prefix all mean what they mean
1689/// everywhere else.
1690async fn resolve_allow(
1691 state: &AppState,
1692 probe: http::Request<bytes::Bytes>,
1693) -> Result<String, postrust_core::Error> {
1694 use postrust_core::api_request::Action;
1695
1696 let api_request = parse_request(
1697 &probe,
1698 state.default_schema(),
1699 state.schemas(),
1700 state.config.db_max_rows,
1701 )?;
1702
1703 let schema_cache = state.schema_cache().await;
1704 match &api_request.action {
1705 Action::RelationInfo(qi) => Ok(relation_allow(schema_cache.require_table(qi)?)),
1706 Action::RoutineInfo { qi, .. } => Ok(routine_allow(schema_cache.get_routines(qi))),
1707 // The schema description, which is read and nothing else.
1708 _ => Ok("OPTIONS,GET,HEAD".to_string()),
1709 }
1710}
1711
1712/// The methods a relation answers, for the `Allow` of an OPTIONS.
1713///
1714/// Reading is always offered. Writing follows what the relation can actually
1715/// do, which for a view is what its triggers give it: a view with only an
1716/// INSERT trigger takes a POST and nothing else.
1717///
1718/// PUT is the exception that is not simply a permission. It replaces one row
1719/// named by its primary key, so a relation without a primary key cannot
1720/// answer it however writable it otherwise is -- which is why an auto-updatable
1721/// view without a key offers POST, PATCH and DELETE but not PUT.
1722///
1723fn relation_allow(table: &postrust_core::schema_cache::Table) -> String {
1724 let mut methods = vec!["OPTIONS", "GET", "HEAD"];
1725
1726 if table.insertable {
1727 methods.push("POST");
1728 }
1729 if table.insertable && table.updatable && !table.pk_cols.is_empty() {
1730 methods.push("PUT");
1731 }
1732 if table.updatable {
1733 methods.push("PATCH");
1734 }
1735 if table.deletable {
1736 methods.push("DELETE");
1737 }
1738
1739 methods.join(",")
1740}
1741
1742/// The methods a function answers, for the `Allow` of an OPTIONS.
1743///
1744/// A volatile function may change data, so it is reachable only by POST. One
1745/// that cannot can also be read, and is offered under GET and HEAD as well.
1746/// An overloaded name is readable when any of its overloads is.
1747fn routine_allow(routines: Option<&Vec<postrust_core::schema_cache::Routine>>) -> String {
1748 let readable = routines.is_some_and(|routines| {
1749 routines.iter().any(|routine| {
1750 !matches!(
1751 routine.volatility,
1752 postrust_core::schema_cache::FuncVolatility::Volatile
1753 )
1754 })
1755 });
1756
1757 match readable {
1758 true => "OPTIONS,GET,HEAD,POST".to_string(),
1759 false => "OPTIONS,POST".to_string(),
1760 }
1761}
1762
1763/// Reshape RPC rows for PostgREST-compatibility mode.
1764///
1765/// `SELECT * FROM func(...)` names its single output column after the function
1766/// (for scalar/`json` returns), which serializes to rows like
1767/// `[{"func": <value>}]`. PostgREST instead returns the bare value. This
1768/// un-nests that wrapper column and reports whether the result should be
1769/// rendered as a single (un-arrayed) value — i.e. when the function is not
1770/// set-returning.
1771///
1772/// The decision is driven by the plan's return-type metadata: composite and
1773/// `record` returns (`RETURNS TABLE`, row types) have real output columns and
1774/// are never un-nested, even when a single column happens to share the
1775/// function's name (e.g. `CREATE FUNCTION foo() RETURNS TABLE(foo int)`).
1776fn unwrap_rpc_rows(
1777 rows: Vec<serde_json::Value>,
1778 call: &CallPlan,
1779) -> (Vec<serde_json::Value>, bool) {
1780 let singular = !call.returns_set;
1781
1782 if call.returns_composite {
1783 return (rows, singular);
1784 }
1785
1786 let fname = call.function.name.as_str();
1787 let unwrapped = rows
1788 .into_iter()
1789 .map(|row| match row {
1790 serde_json::Value::Object(ref map) if map.len() == 1 && map.contains_key(fname) => {
1791 map.get(fname).cloned().unwrap_or(serde_json::Value::Null)
1792 }
1793 other => other,
1794 })
1795 .collect();
1796
1797 (unwrapped, singular)
1798}
1799
1800/// The table a read targets, if the request is a read.
1801fn read_target(
1802 api_request: &postrust_core::api_request::ApiRequest,
1803 db_plan: Option<&DbActionPlan>,
1804 schema_cache: &postrust_core::SchemaCache,
1805) -> Option<postrust_core::api_request::QualifiedIdentifier> {
1806 use postrust_core::api_request::{Action, DbAction};
1807
1808 match &api_request.action {
1809 Action::Db(DbAction::RelationRead { qi, .. }) => Some(qi.clone()),
1810 // The rows a mutation affected are rows of that table, so `?select=`
1811 // reaches through them to related resources exactly as it does on a
1812 // read -- `DELETE /tasks?select=name,project:projects(id)` says what
1813 // was deleted and what it belonged to.
1814 Action::Db(DbAction::RelationMut { qi, .. }) => Some(qi.clone()),
1815 // A function returning a table's rows embeds, and renders, exactly as
1816 // that table does.
1817 //
1818 // Which table that is was settled when the overload was chosen: a
1819 // name may carry several signatures returning different things, and
1820 // asking the cache again by name answers for whichever comes first.
1821 // It also knows the answer for a return type this side cannot read
1822 // off the name at all, such as a domain over a table.
1823 Action::Db(DbAction::Routine { qi, .. }) => match db_plan {
1824 Some(DbActionPlan::Call { read, .. }) => {
1825 read.as_ref().map(|tree| tree.root.from.clone())
1826 }
1827 _ => schema_cache
1828 .routine_returned_table(qi)
1829 .map(|table| table.qualified_identifier()),
1830 },
1831 _ => None,
1832 }
1833}
1834
1835/// One `?or=` or `?and=` over embedded resources, as a `WHERE` fragment.
1836///
1837/// Every leaf asks whether an embed matched, which is an existence test, and
1838/// the tree is what combines them. `None` where any leaf asks something else,
1839/// since half a condition is worse than none.
1840fn embed_logic_predicate(
1841 tree: &postrust_core::api_request::LogicTree,
1842 filterable: &[(String, String)],
1843) -> Option<String> {
1844 use postrust_core::api_request::{IsValue, LogicOperator, LogicTree, Operation};
1845
1846 match tree {
1847 LogicTree::Expr {
1848 negated,
1849 op,
1850 children,
1851 } => {
1852 let rendered = children
1853 .iter()
1854 .map(|child| embed_logic_predicate(child, filterable))
1855 .collect::<Option<Vec<String>>>()?;
1856 let joined = rendered.join(match op {
1857 LogicOperator::And => " AND ",
1858 LogicOperator::Or => " OR ",
1859 });
1860 Some(match negated {
1861 true => format!("NOT ({joined})"),
1862 false => format!("({joined})"),
1863 })
1864 }
1865 LogicTree::Stmt(filter) => {
1866 let (_, exists) = filterable
1867 .iter()
1868 .find(|(name, _)| name == &filter.field.name)?;
1869 // `is.null` asks for parents the embed did not match, so the
1870 // existence test is the negation of it.
1871 let negated = match &filter.op_expr.operation {
1872 Operation::Is(IsValue::Null) => filter.op_expr.negated,
1873 Operation::Is(IsValue::NotNull) => !filter.op_expr.negated,
1874 _ => return None,
1875 };
1876 Some(match negated {
1877 true => exists.clone(),
1878 false => format!("NOT {exists}"),
1879 })
1880 }
1881 }
1882}
1883
1884/// Round a geometry's numbers to nine decimal places.
1885///
1886/// Not cosmetic: nine is what `ST_AsGeoJSON` emits by default, and a geometry
1887/// read through the type's own rendering carries every digit the double has.
1888/// Nine decimal places of latitude is under a tenth of a millimetre, so the
1889/// digits past it describe floating-point representation rather than any
1890/// place on the earth.
1891fn round_coordinates(value: &mut serde_json::Value) {
1892 match value {
1893 serde_json::Value::Array(items) => items.iter_mut().for_each(round_coordinates),
1894 serde_json::Value::Object(fields) => fields.values_mut().for_each(round_coordinates),
1895 serde_json::Value::Number(number) => {
1896 let Some(float) = number.as_f64() else { return };
1897 if float.fract() == 0.0 {
1898 return;
1899 }
1900 // Through the decimal text rather than by arithmetic: scaling by a
1901 // power of ten and rounding back reintroduces exactly the digits
1902 // this is meant to drop.
1903 if let Ok(rounded) = format!("{float:.9}").parse::<f64>() {
1904 if let Some(rounded) = serde_json::Number::from_f64(rounded) {
1905 *number = rounded;
1906 }
1907 }
1908 }
1909 _ => {}
1910 }
1911}
1912
1913/// Where the response's window starts.
1914///
1915/// The same resolution the plan itself used, asked again rather than restated:
1916/// the JSON path and the rendered-media path both report a `Content-Range`,
1917/// and a `Content-Range` that disagrees with the rows underneath it is worse
1918/// than none at all.
1919fn top_level_offset(api_request: &ApiRequest) -> i64 {
1920 postrust_core::plan::resolve_top_level_range(api_request).offset
1921}
1922
1923/// The first column of a row, as the bytes the client should receive.
1924///
1925/// A media type the schema declared is carried by a domain, and a domain is a
1926/// type this side has no decoder for -- so the value arrives as PostgreSQL's
1927/// own text rendering. For text, xml or json that rendering is the value. For
1928/// `bytea` it is `\x` followed by hex, which is a description of the bytes
1929/// and not the bytes, so it is decoded back.
1930fn raw_column(row: &sqlx::postgres::PgRow, base_type: &str) -> Option<Vec<u8>> {
1931 use sqlx::{Row, ValueRef};
1932
1933 let value = row.try_get_raw(0).ok()?;
1934 if value.is_null() {
1935 return None;
1936 }
1937 let bytes = value.as_bytes().ok()?.to_vec();
1938 if base_type != "bytea" {
1939 return Some(bytes);
1940 }
1941 // Text or binary, depending on whether this connection asked for a format
1942 // it could name -- so the `\x` rendering is decoded where it appears and
1943 // the bytes are passed through where they do not.
1944 let Some(hex) = bytes.strip_prefix(b"\\x") else {
1945 return Some(bytes);
1946 };
1947 hex.chunks(2)
1948 .map(|pair| {
1949 let digits = std::str::from_utf8(pair).ok()?;
1950 u8::from_str_radix(digits, 16).ok()
1951 })
1952 .collect::<Option<Vec<u8>>>()
1953 .or(Some(bytes))
1954}
1955
1956/// Whether the selection is the one a table has when none was asked for.
1957///
1958/// A handler declared for a table renders the table's own rows, so it applies
1959/// only where the request did not reshape them. `?select=*` is that shape
1960/// written out; `?select=id` and any embed are not.
1961fn has_default_select(select: &[postrust_core::api_request::SelectItem]) -> bool {
1962 use postrust_core::api_request::SelectItem;
1963
1964 match select {
1965 [] => true,
1966 [SelectItem::Field { field, alias, .. }] => field.name == "*" && alias.is_none(),
1967 _ => false,
1968 }
1969}
1970
1971/// Whether this server renders the type itself, with nothing declared for it.
1972fn renders_without_a_handler(media: &postrust_core::api_request::MediaType) -> bool {
1973 use postrust_core::api_request::MediaType;
1974
1975 matches!(
1976 media,
1977 MediaType::ApplicationJson
1978 | MediaType::Any
1979 | MediaType::TextCsv
1980 | MediaType::GeoJson
1981 | MediaType::SingularJson { .. }
1982 | MediaType::ArrayJson { .. }
1983 | MediaType::OpenApi
1984 )
1985}
1986
1987/// Render rows as a GeoJSON `FeatureCollection`.
1988///
1989/// The geometry arrives already rendered: a user-defined type is rendered by
1990/// the database, and what PostGIS renders a geometry as is GeoJSON -- with a
1991/// `crs` key, which a feature's geometry does not carry. Everything else on
1992/// the row is the feature's properties, the geometry excepted, since a
1993/// feature's geometry is not also one of its properties.
1994fn feature_collection(rows: &[serde_json::Value], column: &str) -> Vec<u8> {
1995 let features: Vec<serde_json::Value> = rows
1996 .iter()
1997 .map(|row| {
1998 let mut properties = row.clone();
1999 let geometry = properties
2000 .as_object_mut()
2001 .and_then(|fields| fields.remove(column))
2002 .map(|mut geometry| {
2003 if let Some(fields) = geometry.as_object_mut() {
2004 fields.remove("crs");
2005 }
2006 round_coordinates(&mut geometry);
2007 geometry
2008 })
2009 .unwrap_or(serde_json::Value::Null);
2010 serde_json::json!({
2011 "type": "Feature",
2012 "geometry": geometry,
2013 "properties": properties,
2014 })
2015 })
2016 .collect();
2017 serde_json::to_vec(&serde_json::json!({
2018 "type": "FeatureCollection",
2019 "features": features,
2020 }))
2021 .unwrap_or_default()
2022}
2023
2024/// The key under which a GeoJSON feature's geometry is read.
2025///
2026/// A table may carry more than one geometry column, and `?select=` is what
2027/// says which of them the request is about: `select=id,name,range_area` asks
2028/// for a collection of areas, not for whichever geometry the catalogue
2029/// happens to list first. The name wanted is the one the response uses, so an
2030/// aliased column is found under its alias -- which is also the key that has
2031/// to come back out of `properties`, since a feature's geometry is not also
2032/// one of its properties.
2033///
2034/// `None` means the rows carry no geometry at all, which is not a GeoJSON
2035/// response with something missing from it but a request that cannot be
2036/// answered.
2037fn geometry_key(
2038 table: &postrust_core::schema_cache::Table,
2039 select: &[postrust_core::api_request::SelectItem],
2040) -> Option<String> {
2041 use postrust_core::api_request::SelectItem;
2042
2043 let is_geometry = |name: &str| {
2044 table
2045 .get_column(name)
2046 .is_some_and(|c| matches!(c.nominal_type.as_str(), "geometry" | "geography"))
2047 };
2048
2049 // `*` and a named column can both appear in one selection, and a column
2050 // named outright is the one meant even so: `select=*,range_area` is a
2051 // request about areas.
2052 let mut selects_every_column = select.is_empty();
2053 for item in select {
2054 if let SelectItem::Field { field, alias, .. } = item {
2055 if field.name == "*" {
2056 selects_every_column = true;
2057 } else if is_geometry(&field.name) {
2058 return Some(alias.clone().unwrap_or_else(|| field.name.clone()));
2059 }
2060 }
2061 }
2062
2063 if !selects_every_column {
2064 return None;
2065 }
2066 table
2067 .columns
2068 .values()
2069 .find(|c| matches!(c.nominal_type.as_str(), "geometry" | "geography"))
2070 .map(|c| c.name.clone())
2071}
2072
2073/// Whether any part of the selection, at any depth, asks for an aggregate.
2074fn selects_an_aggregate(api_request: &ApiRequest) -> bool {
2075 use postrust_core::api_request::SelectItem;
2076
2077 fn walk(items: &[SelectItem]) -> bool {
2078 items.iter().any(|item| match item {
2079 SelectItem::Field { aggregate, .. } => aggregate.is_some(),
2080 SelectItem::Relation { select, .. } => walk(select),
2081 SelectItem::SpreadRelation { select, .. } => walk(select),
2082 })
2083 }
2084
2085 walk(&api_request.query_params.select)
2086}
2087
2088/// Whether this request is a read, and so may run read-only.
2089///
2090/// It is the method that decides, not the plan: calling a function is a read
2091/// over GET and a write over POST, and the same function may be either. Asking
2092/// the plan instead would class every call as a read and refuse the writes a
2093/// POST is entitled to make.
2094fn is_read_only(api_request: &ApiRequest) -> bool {
2095 use postrust_core::api_request::{Action, DbAction, InvokeMethod};
2096
2097 match &api_request.action {
2098 Action::Db(DbAction::RelationRead { .. }) | Action::Db(DbAction::SchemaRead { .. }) => true,
2099 Action::Db(DbAction::Routine { invoke_method, .. }) => {
2100 matches!(invoke_method, InvokeMethod::InvRead { .. })
2101 }
2102 Action::Db(DbAction::RelationMut { .. }) => false,
2103 // Metadata only; nothing is executed against the database.
2104 Action::RelationInfo(_) | Action::RoutineInfo { .. } | Action::SchemaInfo => true,
2105 }
2106}
2107
2108/// The plan's SQL with its page removed, for counting what the filters match.
2109///
2110/// Only reads and calls are counted. A mutation's `Content-Range` reports the
2111/// rows it affected, which is the result set itself.
2112fn unpaged_sql(
2113 db_plan: &postrust_core::plan::DbActionPlan,
2114 role: &str,
2115) -> Result<Option<String>, postrust_core::Error> {
2116 use postrust_core::plan::{ActionPlan, DbActionPlan};
2117
2118 let unpaged = match db_plan {
2119 DbActionPlan::Read(tree) => {
2120 let mut tree = tree.clone();
2121 tree.root.range = Default::default();
2122 DbActionPlan::Read(tree)
2123 }
2124 DbActionPlan::Call { call, read } => {
2125 let read = read.as_ref().map(|tree| {
2126 let mut tree = tree.clone();
2127 tree.root.range = Default::default();
2128 tree
2129 });
2130 DbActionPlan::Call {
2131 call: call.clone(),
2132 read,
2133 }
2134 }
2135 DbActionPlan::MutateRead { .. } => return Ok(None),
2136 };
2137
2138 let query = postrust_core::query::build_query(&ActionPlan::Db(unpaged), Some(role))?;
2139 Ok(query.has_main().then(|| query.build_main().0))
2140}
2141
2142/// The total the `Prefer: count` asked for, or `None` when it asked for none.
2143///
2144/// `exact` counts the rows the filters match. `planned` asks the query planner
2145/// what it expects without running anything, which is cheap on a large table
2146/// and correspondingly rough. `estimated` is the planner's guess only where it
2147/// matters: below the server's row ceiling the exact count is already paid for,
2148/// so it is used, and above it the larger of the two is reported -- an estimate
2149/// that came in under a count we have actually taken would be plainly wrong.
2150async fn resolve_count(
2151 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2152 preference: &postrust_core::api_request::PreferCount,
2153 count_sql: &str,
2154 params: &[postrust_sql::SqlParam],
2155 max_rows: Option<i64>,
2156) -> Result<Option<i64>, postrust_core::Error> {
2157 use postrust_core::api_request::PreferCount;
2158
2159 match preference {
2160 PreferCount::Exact => exact_count(tx, count_sql, params).await,
2161 PreferCount::Planned => planned_count(tx, count_sql, params).await,
2162 PreferCount::Estimated => match (exact_count(tx, count_sql, params).await?, max_rows) {
2163 (Some(exact), Some(ceiling)) if exact <= ceiling => Ok(Some(exact)),
2164 (Some(exact), _) => {
2165 let planned = planned_count(tx, count_sql, params).await?;
2166 Ok(Some(exact.max(planned.unwrap_or(exact))))
2167 }
2168 (None, _) => planned_count(tx, count_sql, params).await,
2169 },
2170 }
2171}
2172
2173/// The number of rows the filters match.
2174async fn exact_count(
2175 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2176 count_sql: &str,
2177 params: &[postrust_sql::SqlParam],
2178) -> Result<Option<i64>, postrust_core::Error> {
2179 use sqlx::Row;
2180
2181 let sql = format!("SELECT count(*) FROM ({}) AS pgrst_count", count_sql);
2182 let row = bind_params(sqlx::query(&sql), params)
2183 .fetch_one(&mut **tx)
2184 .await
2185 .map_err(map_sqlx_error)?;
2186 Ok(row.try_get::<i64, _>(0).ok())
2187}
2188
2189/// The number of rows the query planner expects, without running the query.
2190async fn planned_count(
2191 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
2192 count_sql: &str,
2193 params: &[postrust_sql::SqlParam],
2194) -> Result<Option<i64>, postrust_core::Error> {
2195 use sqlx::Row;
2196
2197 let sql = format!("EXPLAIN (FORMAT JSON) {}", count_sql);
2198 let row = bind_params(sqlx::query(&sql), params)
2199 .fetch_one(&mut **tx)
2200 .await
2201 .map_err(map_sqlx_error)?;
2202 let plan: serde_json::Value = row.try_get(0).unwrap_or(serde_json::Value::Null);
2203 Ok(plan
2204 .get(0)
2205 .and_then(|node| node.get("Plan"))
2206 .and_then(|node| node.get("Plan Rows"))
2207 .and_then(serde_json::Value::as_i64))
2208}
2209
2210/// Whether this plan changes data.
2211fn is_mutation(db_plan: &postrust_core::plan::DbActionPlan) -> bool {
2212 matches!(
2213 db_plan,
2214 postrust_core::plan::DbActionPlan::MutateRead { .. }
2215 )
2216}
2217
2218/// Whether the caller asked for the affected rows back.
2219fn wants_representation(api_request: &ApiRequest) -> bool {
2220 matches!(
2221 api_request.preferences.representation,
2222 postrust_core::api_request::PreferRepresentation::Full
2223 )
2224}
2225
2226/// Whether a created row's address is worth reporting.
2227///
2228/// `Prefer: return=headers-only` asks for exactly that and nothing else, and
2229/// is the only request PostgREST answers with a `Location`. A caller taking
2230/// the row back reads the key out of the body; a caller wanting neither asked
2231/// for a minimal response and gets one.
2232fn wants_location(api_request: &ApiRequest) -> bool {
2233 matches!(
2234 api_request.preferences.representation,
2235 postrust_core::api_request::PreferRepresentation::HeadersOnly
2236 )
2237}
2238
2239/// The status PostgREST gives a successful mutation, or `None` for anything
2240/// that isn't one.
2241///
2242/// An insert reports 201 Created. An update or delete reports 204 No Content
2243/// unless the caller asked for the rows back, in which case there is content
2244/// to report and it is a plain 200.
2245fn mutation_status(
2246 db_plan: &postrust_core::plan::DbActionPlan,
2247 api_request: &ApiRequest,
2248 created_a_row: bool,
2249 // Whether the statement was in a position to say. A write with no conflict
2250 // clause could only have created rows, and one on a relation that will not
2251 // give up `xmax` cannot tell either way -- in both cases there is nothing
2252 // to report but the creation.
2253 could_have_merged: bool,
2254) -> Option<StatusCode> {
2255 use postrust_core::api_request::PreferResolution;
2256 use postrust_core::plan::{DbActionPlan, MutatePlan};
2257
2258 let DbActionPlan::MutateRead { mutate, .. } = db_plan else {
2259 return None;
2260 };
2261
2262 let merging = matches!(
2263 api_request.preferences.resolution,
2264 Some(PreferResolution::MergeDuplicates)
2265 );
2266
2267 Some(match mutate {
2268 // A `PUT` names the row it is writing, so from the client's side
2269 // nothing was discovered by the response -- it reports the outcome
2270 // only where it asked to see the row.
2271 MutatePlan::Insert { .. } if is_upsert(api_request) => {
2272 match (
2273 wants_representation(api_request),
2274 created_a_row || !could_have_merged,
2275 ) {
2276 (false, _) => StatusCode::NO_CONTENT,
2277 (true, true) => StatusCode::CREATED,
2278 (true, false) => StatusCode::OK,
2279 }
2280 }
2281 // A `POST` that merged into every row it touched created nothing, and
2282 // 201 would be a promise that something new is at the `Location`.
2283 MutatePlan::Insert { .. } if merging && could_have_merged && !created_a_row => {
2284 StatusCode::OK
2285 }
2286 MutatePlan::Insert { .. } => StatusCode::CREATED,
2287 _ => match wants_representation(api_request) {
2288 true => StatusCode::OK,
2289 false => StatusCode::NO_CONTENT,
2290 },
2291 })
2292}
2293
2294/// Ensure every embedded relation's join column is selected on the parent.
2295///
2296/// Returns the columns that were added purely to make the join possible, so
2297/// they can be removed from the response afterwards. An empty select list means
2298/// "all columns", in which case nothing needs adding.
2299fn add_embed_join_columns(
2300 api_request: &mut postrust_core::api_request::ApiRequest,
2301 schema_cache: &postrust_core::SchemaCache,
2302) -> Result<Vec<String>, postrust_core::Error> {
2303 use postrust_core::api_request::{Field, SelectItem};
2304
2305 let Some(parent_qi) = read_target(api_request, None, schema_cache) else {
2306 return Ok(Vec::new());
2307 };
2308
2309 let select = &api_request.query_params.select;
2310 if select.is_empty() {
2311 return Ok(Vec::new());
2312 }
2313
2314 // `*` already names every column, so no join column needs adding -- and
2315 // adding one anyway selects it twice, which makes `src.*` ambiguous. The
2316 // relations are still walked: a computed relationship needs the parent's
2317 // row carried out, and `*` does not provide that.
2318 let selects_everything = select
2319 .iter()
2320 .any(|item| matches!(item, SelectItem::Field { field, .. } if field.name == "*"));
2321
2322 // Only a column selected under its own name serves as a join key. An
2323 // aliased one -- `myId:id` -- arrives in the result as `myId`, so the
2324 // correlation would look for a column the inner query no longer has; the
2325 // same goes for one that was cast or reached into with a JSON path.
2326 let selected: std::collections::HashSet<String> = select
2327 .iter()
2328 .filter_map(|item| match item {
2329 SelectItem::Field {
2330 field,
2331 aggregate: None,
2332 cast: None,
2333 alias: None,
2334 ..
2335 } if field.json_path.is_empty() => Some(field.name.clone()),
2336 _ => None,
2337 })
2338 .collect();
2339
2340 let mut added = Vec::new();
2341
2342 for item in select.clone() {
2343 // A spread joins on a parent column exactly as a plain embed does, so
2344 // it needs that column selected just the same. Without it the parent
2345 // row has no key to match children against and every spread column
2346 // comes back null.
2347 let (relation, hint) = match &item {
2348 SelectItem::Relation { relation, hint, .. } => (relation, hint),
2349 SelectItem::SpreadRelation { relation, hint, .. } => (relation, hint),
2350 SelectItem::Field { .. } => continue,
2351 };
2352
2353 let rel = schema_cache
2354 .find_relationship(&parent_qi, relation, hint.as_deref(), &parent_qi.schema)?
2355 .ok_or_else(|| {
2356 schema_cache.relationship_not_found(
2357 &parent_qi,
2358 relation,
2359 hint.as_deref(),
2360 &parent_qi.schema,
2361 )
2362 })?;
2363
2364 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
2365
2366 // A computed relationship joins on nothing -- the parent row is the
2367 // function's argument. That row has to be carried out of the inner
2368 // query, which is a column of its own rather than a join key. After a
2369 // mutation it is carried out of the statement itself, by `RETURNING`,
2370 // because that is the last place the row still has the table's type.
2371 if plan.function.is_some() {
2372 if !added.iter().any(|c| c == PARENT_ROW_COLUMN) {
2373 added.push(PARENT_ROW_COLUMN.to_string());
2374 }
2375 continue;
2376 }
2377
2378 // Every column the join is on, not just the first: a foreign key over
2379 // several columns correlates on all of them, and one left unselected
2380 // is a column the correlation cannot see.
2381 let join_columns: Vec<String> = match plan.columns.is_empty() {
2382 true => vec![plan.local_column.clone()],
2383 false => plan
2384 .columns
2385 .iter()
2386 .map(|(local, _)| local.clone())
2387 .collect(),
2388 };
2389
2390 if selects_everything {
2391 continue;
2392 }
2393
2394 for column in join_columns {
2395 if !selected.contains(&column) && !added.contains(&column) {
2396 api_request.query_params.select.push(SelectItem::Field {
2397 field: Field::simple(&column),
2398 aggregate: None,
2399 aggregate_cast: None,
2400 cast: None,
2401 alias: None,
2402 });
2403 added.push(column);
2404 }
2405 }
2406 }
2407
2408 Ok(added)
2409}
2410
2411/// Build the SELECT-list expressions that embed relations in one query.
2412///
2413/// Returns one `(field_name, expression)` per requested relation. Each
2414/// expression is a correlated subselect yielding JSON, so the whole tree comes
2415/// back from the parent query rather than from a query per relation per level.
2416///
2417/// `alias_counter` hands out a distinct alias per level, so a self-referential
2418/// relationship stays unambiguous.
2419/// Filters addressed at embedded resources, and the parameters they bind.
2420///
2421/// The embed expressions are assembled as SQL text and wrapped around a main
2422/// query that already owns `$1..$n`, so any placeholder introduced here has to
2423/// be renumbered past that point and its value appended in the same order.
2424struct EmbedFilters<'a> {
2425 /// Every path-scoped filter on the request, e.g. `(["clients"], id=eq.1)`.
2426 filters: &'a [(
2427 postrust_core::api_request::EmbedPath,
2428 postrust_core::api_request::Filter,
2429 )],
2430 /// Values bound by the predicates built so far, in placeholder order.
2431 params: Vec<postrust_sql::SqlParam>,
2432 /// How many parameters the main query already uses.
2433 base: usize,
2434 /// Ordering asked of each embedded resource, by path.
2435 orders: &'a [(
2436 postrust_core::api_request::EmbedPath,
2437 Vec<postrust_core::api_request::OrderTerm>,
2438 )],
2439 /// Ranges asked of each embedded resource, by dotted path.
2440 ranges: &'a std::collections::HashMap<String, postrust_core::api_request::Range>,
2441 /// `and=`/`or=` groups asked of each embedded resource, by path.
2442 logic: &'a [(
2443 postrust_core::api_request::EmbedPath,
2444 postrust_core::api_request::LogicTree,
2445 )],
2446 /// Row cap applied to each embedded resource.
2447 max_rows: Option<i64>,
2448 /// Source of unique subquery aliases across the whole embed tree.
2449 alias_counter: usize,
2450 /// How an embedded resource is rendered.
2451 ///
2452 /// `Verbatim` everywhere except under a media handler that stringifies the
2453 /// whole row: there each column is rendered as *text*, and a `json` column
2454 /// renders exactly as written where PostgREST's `jsonb` one renders
2455 /// normalised. See [`postrust_core::EmbedRendering`].
2456 rendering: postrust_core::EmbedRendering,
2457}
2458
2459/// Renumber the placeholders in `sql` so they start after `offset`.
2460///
2461/// Scans rather than string-replaces: a naive replacement of `$1` would also
2462/// corrupt the `$1` inside `$10`.
2463fn shift_placeholders(sql: &str, offset: usize) -> String {
2464 let mut out = String::with_capacity(sql.len());
2465 let mut chars = sql.char_indices().peekable();
2466
2467 while let Some((i, ch)) = chars.next() {
2468 if ch != '$' {
2469 out.push(ch);
2470 continue;
2471 }
2472 let start = i + 1;
2473 let mut end = start;
2474 while let Some((j, d)) = chars.peek() {
2475 if d.is_ascii_digit() {
2476 end = j + d.len_utf8();
2477 chars.next();
2478 } else {
2479 break;
2480 }
2481 }
2482 match sql[start..end].parse::<usize>() {
2483 Ok(n) => out.push_str(&format!("${}", n + offset)),
2484 Err(_) => out.push('$'),
2485 }
2486 }
2487
2488 out
2489}
2490
2491impl EmbedFilters<'_> {
2492 /// A fresh alias, unique across the embed tree.
2493 fn next_alias(&mut self) -> String {
2494 self.alias_counter += 1;
2495 format!("e{}", self.alias_counter)
2496 }
2497
2498 /// The `WHERE` fragment for one embedded resource, or `None` if unfiltered.
2499 /// The row window an embedded resource was asked for.
2500 ///
2501 /// The server's own cap still applies, so an embed cannot be asked for
2502 /// more rows than the server is willing to return.
2503 fn range_for(&self, path: &[String], names: &[String]) -> (Option<i64>, i64) {
2504 let range = self
2505 .ranges
2506 .get(&path.join("."))
2507 .or_else(|| self.ranges.get(&names.join(".")));
2508 let requested = range.and_then(|r| r.limit);
2509 let offset = range.map(|r| r.offset).unwrap_or(0);
2510 let limit = match (requested, self.max_rows) {
2511 (Some(limit), Some(cap)) => Some(limit.min(cap)),
2512 (Some(limit), None) => Some(limit),
2513 (None, cap) => cap,
2514 };
2515 (limit, offset)
2516 }
2517
2518 fn predicate_for(
2519 &mut self,
2520 path: &[String],
2521 names: &[String],
2522 child_qi: &postrust_core::api_request::QualifiedIdentifier,
2523 schema_cache: &postrust_core::SchemaCache,
2524 ) -> Result<Option<String>, postrust_core::Error> {
2525 let addresses = |candidate: &[String]| candidate == path || candidate == names;
2526
2527 let matching: Vec<_> = self
2528 .filters
2529 .iter()
2530 .filter(|(filter_path, _)| addresses(filter_path))
2531 .map(|(_, filter)| filter.clone())
2532 .collect();
2533
2534 let matching_logic: Vec<_> = self
2535 .logic
2536 .iter()
2537 .filter(|(logic_path, _)| addresses(logic_path))
2538 .map(|(_, tree)| tree.clone())
2539 .collect();
2540
2541 if matching.is_empty() && matching_logic.is_empty() {
2542 return Ok(None);
2543 }
2544
2545 let table = schema_cache.get_table(child_qi);
2546 let mut parts = Vec::with_capacity(matching.len());
2547
2548 for filter in &matching {
2549 // Inside the child's subselect an unqualified name binds to the
2550 // child, but only because the child has that column: an unknown
2551 // name would resolve outward to the correlated parent and filter
2552 // the wrong table silently. Refuse instead.
2553 let column = table
2554 .and_then(|t| t.columns.get(&filter.field.name))
2555 .ok_or_else(|| {
2556 postrust_core::Error::ColumnNotFound(format!(
2557 "{}.{}",
2558 child_qi.name, filter.field.name
2559 ))
2560 })?;
2561
2562 let frag =
2563 postrust_core::query::QueryBuilder::filter_sql(filter, &column.nominal_type)?;
2564 parts.push(shift_placeholders(
2565 frag.sql(),
2566 self.base + self.params.len(),
2567 ));
2568 self.params.extend(frag.params().iter().cloned());
2569 }
2570
2571 for tree in matching_logic {
2572 // A name the child does not have would bind outward to the
2573 // correlated parent and filter the wrong table, exactly as a plain
2574 // filter would, so the tree is checked before it is rendered.
2575 let mut unknown = None;
2576 check_logic_columns(&tree, table, &mut unknown);
2577 if let Some(name) = unknown {
2578 return Err(postrust_core::Error::ColumnNotFound(format!(
2579 "{}.{}",
2580 child_qi.name, name
2581 )));
2582 }
2583
2584 let resolver = |name: &str| -> String {
2585 table
2586 .and_then(|t| t.get_column(name))
2587 .map(|c| c.nominal_type.clone())
2588 .unwrap_or_else(|| "text".to_string())
2589 };
2590 let frag = postrust_core::query::QueryBuilder::logic_sql(&tree, resolver)?;
2591 parts.push(shift_placeholders(
2592 frag.sql(),
2593 self.base + self.params.len(),
2594 ));
2595 self.params.extend(frag.params().iter().cloned());
2596 }
2597
2598 Ok(Some(format!("({})", parts.join(" AND "))))
2599 }
2600}
2601
2602/// Record the first column a logic tree names that the table does not have.
2603fn check_logic_columns(
2604 tree: &postrust_core::api_request::LogicTree,
2605 table: Option<&postrust_core::schema_cache::Table>,
2606 unknown: &mut Option<String>,
2607) {
2608 use postrust_core::api_request::LogicTree;
2609
2610 match tree {
2611 LogicTree::Expr { children, .. } => {
2612 for child in children {
2613 check_logic_columns(child, table, unknown);
2614 }
2615 }
2616 LogicTree::Stmt(filter) => {
2617 if unknown.is_none()
2618 && table
2619 .map(|t| t.get_column(&filter.field.name).is_none())
2620 .unwrap_or(true)
2621 {
2622 *unknown = Some(filter.field.name.clone());
2623 }
2624 }
2625 }
2626}
2627
2628/// One level of the embed tree.
2629#[derive(Default)]
2630struct EmbedLevel {
2631 /// `(response key, SQL expression)` for each relation at this level.
2632 expressions: Vec<(String, String)>,
2633 /// `EXISTS` predicates from `!inner` on these relations. They reference
2634 /// the alias of the level *above*, so the caller applies them.
2635 inner_joins: Vec<String>,
2636 /// `ORDER BY` expressions for terms naming an embedded resource.
2637 orders: Vec<String>,
2638 /// Each embed under the name the client used for it, with its
2639 /// expression. A spread's response key is internal, so this is what a
2640 /// filter naming the embed -- `?clients=is.null` -- is matched against.
2641 filterable: Vec<(String, String)>,
2642 /// For each spread, the keys it contributes to its parent.
2643 ///
2644 /// Needed because a spread that matched nothing still carries them --
2645 /// as nulls, or as empty arrays for a to-many -- and by then there is no
2646 /// row to read the names off.
2647 spread_columns: Vec<(String, Vec<String>)>,
2648 /// Whether this level had any relation to embed at all.
2649 ///
2650 /// Distinct from having produced an expression: `clients()` is a relation
2651 /// that deliberately contributes nothing, and the fallback path must not
2652 /// take an absent expression for a relation the single-query form could
2653 /// not handle.
2654 saw_relations: bool,
2655}
2656
2657/// The keys a spread's selection contributes to the object above it.
2658///
2659/// `*` names every column of the related table, an alias renames the column it
2660/// precedes, and a nested spread contributes its own keys, having already been
2661/// flattened into this one.
2662fn spread_output_names(
2663 schema_cache: &postrust_core::SchemaCache,
2664 child_qi: &postrust_core::api_request::QualifiedIdentifier,
2665 select: &[postrust_core::api_request::SelectItem],
2666) -> Vec<String> {
2667 use postrust_core::api_request::SelectItem;
2668
2669 let mut names = Vec::new();
2670 for item in select {
2671 match item {
2672 SelectItem::Field { field, .. } if field.name == "*" => {
2673 if let Some(table) = schema_cache.get_table(child_qi) {
2674 names.extend(table.columns.keys().cloned());
2675 }
2676 }
2677 SelectItem::Field { field, alias, .. } => {
2678 names.push(alias.clone().unwrap_or_else(|| field.name.clone()));
2679 }
2680 SelectItem::Relation {
2681 relation, alias, ..
2682 } => names.push(alias.clone().unwrap_or_else(|| relation.clone())),
2683 SelectItem::SpreadRelation {
2684 relation, select, ..
2685 } => {
2686 if let Some(rel) = schema_cache
2687 .find_relationship(child_qi, relation, None, &child_qi.schema)
2688 .ok()
2689 .flatten()
2690 {
2691 let target = rel.foreign_table().clone();
2692 names.extend(spread_output_names(schema_cache, &target, select));
2693 }
2694 }
2695 }
2696 }
2697 names
2698}
2699
2700/// Dissolve spread embeds into the objects that contain them.
2701///
2702/// A spread arrives as an ordinary JSON column under a reserved name. This
2703/// walks the response and, wherever it finds one, moves its columns up into
2704/// the surrounding object and drops the name.
2705///
2706/// It recurses first, so a spread nested inside another is already flattened
2707/// by the time its parent is -- which is what carries a grandchild's columns
2708/// all the way up.
2709///
2710/// Spreading a to-many gives each column an array of that column's values
2711/// across the matched rows, rather than an array of objects.
2712fn flatten_spreads(
2713 value: &mut serde_json::Value,
2714 columns: &std::collections::HashMap<String, Vec<String>>,
2715) {
2716 match value {
2717 serde_json::Value::Array(items) => {
2718 for item in items {
2719 flatten_spreads(item, columns);
2720 }
2721 }
2722 serde_json::Value::Object(object) => {
2723 for (_, child) in object.iter_mut() {
2724 flatten_spreads(child, columns);
2725 }
2726
2727 let spread_keys: Vec<String> = object
2728 .keys()
2729 .filter(|key| key.starts_with(SPREAD_KEY_PREFIX))
2730 .cloned()
2731 .collect();
2732
2733 for key in spread_keys {
2734 let Some(spread) = object.remove(&key) else {
2735 continue;
2736 };
2737 let names = columns.get(&key).cloned().unwrap_or_default();
2738 match spread {
2739 serde_json::Value::Object(columns) => {
2740 for (name, column) in columns {
2741 object.insert(name, column);
2742 }
2743 }
2744 // A to-many: one array per column rather than an array of
2745 // objects, so the rows are transposed. With no rows the
2746 // columns are still named, each holding an empty array.
2747 serde_json::Value::Array(rows) => {
2748 for name in names {
2749 let column = rows
2750 .iter()
2751 .map(|row| {
2752 row.get(&name).cloned().unwrap_or(serde_json::Value::Null)
2753 })
2754 .collect();
2755 object.insert(name, serde_json::Value::Array(column));
2756 }
2757 }
2758 // The relationship matched nothing. The keys are still
2759 // the client's to expect, so they are there, holding null.
2760 _ => {
2761 for name in names {
2762 object.insert(name, serde_json::Value::Null);
2763 }
2764 }
2765 }
2766 }
2767 }
2768 _ => {}
2769 }
2770}
2771
2772/// The relation an embed names, found by the name the response uses.
2773fn embedded_by_name<'a>(
2774 select: &'a [postrust_core::api_request::SelectItem],
2775 name: &str,
2776) -> Option<(&'a str, Option<&'a str>)> {
2777 use postrust_core::api_request::SelectItem;
2778
2779 select.iter().find_map(|item| match item {
2780 SelectItem::Relation {
2781 relation,
2782 alias,
2783 hint,
2784 ..
2785 } if alias.as_deref().unwrap_or(relation) == name => {
2786 Some((relation.as_str(), hint.as_deref()))
2787 }
2788 // A spread has no alias -- nothing is being named -- so it answers to
2789 // the relation's own name.
2790 SelectItem::SpreadRelation { relation, hint, .. } if relation == name => {
2791 Some((relation.as_str(), hint.as_deref()))
2792 }
2793 _ => None,
2794 })
2795}
2796
2797/// Build the `ORDER BY` expressions for terms naming an embedded resource.
2798///
2799/// `order=clients(name)` orders by a column the parent does not have, so each
2800/// term becomes a scalar subselect fetched per parent row -- the same shape as
2801/// the embed itself, reduced to one column.
2802fn build_embed_orders(
2803 schema_cache: &postrust_core::SchemaCache,
2804 parent_qi: &postrust_core::api_request::QualifiedIdentifier,
2805 order: &[postrust_core::plan::CoercibleOrderTerm],
2806 select: &[postrust_core::api_request::SelectItem],
2807 ctx: &mut EmbedFilters<'_>,
2808) -> Result<Vec<String>, postrust_core::Error> {
2809 let mut orders = Vec::new();
2810
2811 for term in order.iter() {
2812 let Some(relation) = &term.relation else {
2813 continue;
2814 };
2815
2816 // `order=` names a resource this request embedded, by the name the
2817 // response gives it -- so `pros:projects(*)` is ordered on as `pros`,
2818 // and the table it came from is what the relationship is looked up
2819 // by. A name the selection does not carry is not an embedded resource
2820 // here, whether or not the schema has a table by that name.
2821 let embedded = embedded_by_name(select, relation)
2822 .ok_or_else(|| postrust_core::Error::NotAnEmbeddedResource(relation.clone()))?;
2823
2824 let rel = schema_cache
2825 .find_relationship(parent_qi, embedded.0, embedded.1, &parent_qi.schema)?
2826 .ok_or_else(|| {
2827 schema_cache.relationship_not_found(
2828 parent_qi,
2829 embedded.0,
2830 embedded.1,
2831 &parent_qi.schema,
2832 )
2833 })?;
2834
2835 // A resource that yields many rows per parent has no single value to
2836 // order on, so there is nothing the request could mean.
2837 if !rel.is_to_one() {
2838 return Err(postrust_core::Error::RelatedOrderNotPossible {
2839 origin: parent_qi.name.clone(),
2840 relation: relation.clone(),
2841 });
2842 }
2843
2844 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
2845
2846 let child_alias = ctx.next_alias();
2847
2848 // The column belongs to the related table, so its type has to be read
2849 // from there. Resolved against the parent it would be unknown, and an
2850 // unknown type is one this process asks the database to render -- for
2851 // a `jsonb` column that would wrap it in `to_jsonb` and then reach
2852 // into the wrapper.
2853 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
2854 &plan.foreign_schema,
2855 &plan.foreign_table,
2856 );
2857 let mut field = term.field.clone();
2858 field.to_json = !field.json_path.is_empty()
2859 && !schema_cache
2860 .get_table(&child_qi)
2861 .and_then(|table| table.get_column(&field.name))
2862 .is_some_and(|column| matches!(column.data_type.as_str(), "json" | "jsonb"));
2863
2864 let column =
2865 postrust_core::query::QueryBuilder::qualified_column_sql(Some(&child_alias), &field);
2866 let mut expression =
2867 plan.order_expression("src", PARENT_ROW_COLUMN_REF, &child_alias, &column);
2868
2869 match term.direction {
2870 Some(postrust_core::api_request::OrderDirection::Desc) => expression.push_str(" DESC"),
2871 Some(postrust_core::api_request::OrderDirection::Asc) => expression.push_str(" ASC"),
2872 None => {}
2873 }
2874 match term.nulls {
2875 Some(postrust_core::api_request::OrderNulls::First) => {
2876 expression.push_str(" NULLS FIRST")
2877 }
2878 Some(postrust_core::api_request::OrderNulls::Last) => {
2879 expression.push_str(" NULLS LAST")
2880 }
2881 None => {}
2882 }
2883
2884 orders.push(expression);
2885 }
2886
2887 Ok(orders)
2888}
2889
2890/// The relationship an `order=relation(column)` term names, ready to order by.
2891///
2892/// Resolving it is the same work whether the term sits at the top level or
2893/// inside an embed, and it is the same mistake to skip: a relation term
2894/// rendered as a bare column name orders by a column of the wrong table --
2895/// silently, where the parent happens to have a column of that name, and with
2896/// `column "cost" does not exist` where it does not.
2897fn related_order_target(
2898 schema_cache: &postrust_core::SchemaCache,
2899 parent_qi: &postrust_core::api_request::QualifiedIdentifier,
2900 select: &[postrust_core::api_request::SelectItem],
2901 relation: &str,
2902 ctx: &mut EmbedFilters<'_>,
2903) -> Result<
2904 (
2905 postrust_core::embed::EmbedPlan,
2906 String,
2907 postrust_core::api_request::QualifiedIdentifier,
2908 ),
2909 postrust_core::Error,
2910> {
2911 // `order=` names a resource this request embedded, by the name the
2912 // response gives it -- so `pros:projects(*)` is ordered on as `pros`, and
2913 // the table it came from is what the relationship is looked up by. A name
2914 // the selection does not carry is not an embedded resource here, whether
2915 // or not the schema has a table by that name.
2916 let embedded = embedded_by_name(select, relation)
2917 .ok_or_else(|| postrust_core::Error::NotAnEmbeddedResource(relation.to_string()))?;
2918
2919 let rel = schema_cache
2920 .find_relationship(parent_qi, embedded.0, embedded.1, &parent_qi.schema)?
2921 .ok_or_else(|| {
2922 schema_cache.relationship_not_found(
2923 parent_qi,
2924 embedded.0,
2925 embedded.1,
2926 &parent_qi.schema,
2927 )
2928 })?;
2929
2930 // A resource that yields many rows per parent has no single value to order
2931 // on, so there is nothing the request could mean.
2932 if !rel.is_to_one() {
2933 return Err(postrust_core::Error::RelatedOrderNotPossible {
2934 origin: parent_qi.name.clone(),
2935 relation: relation.to_string(),
2936 });
2937 }
2938
2939 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
2940 let child_alias = ctx.next_alias();
2941 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
2942 &plan.foreign_schema,
2943 &plan.foreign_table,
2944 );
2945
2946 Ok((plan, child_alias, child_qi))
2947}
2948
2949/// The `ORDER BY` an embedded resource was asked for, if any.
2950///
2951/// `clients.order=name.desc` orders the rows inside the embed, which is a
2952/// property of the child's own subselect rather than of the parent.
2953/// `tasks.order=projects(id).desc` orders them by a column of a table the
2954/// child embeds in turn -- so it is resolved against the child, and rendered
2955/// as the same correlated subselect the top level uses.
2956#[allow(clippy::too_many_arguments)] // each names one part of where the embed sits
2957fn embed_order_sql(
2958 schema_cache: &postrust_core::SchemaCache,
2959 child_qi: &postrust_core::api_request::QualifiedIdentifier,
2960 child_select: &[postrust_core::api_request::SelectItem],
2961 child_alias: &str,
2962 ctx: &mut EmbedFilters<'_>,
2963 path: &[String],
2964 names: &[String],
2965) -> Result<Option<String>, postrust_core::Error> {
2966 use postrust_core::api_request::{OrderDirection, OrderNulls, OrderTerm};
2967
2968 let terms: Vec<OrderTerm> = ctx
2969 .orders
2970 .iter()
2971 .filter(|(p, _)| p.as_slice() == path || p.as_slice() == names)
2972 .flat_map(|(_, terms)| terms.iter().cloned())
2973 .collect();
2974
2975 if terms.is_empty() {
2976 return Ok(None);
2977 }
2978
2979 let mut rendered = Vec::with_capacity(terms.len());
2980 for term in &terms {
2981 let (field, direction, nulls) = match term {
2982 OrderTerm::Field {
2983 field,
2984 direction,
2985 nulls,
2986 }
2987 | OrderTerm::Relation {
2988 field,
2989 direction,
2990 nulls,
2991 ..
2992 } => (field, direction, nulls),
2993 };
2994
2995 let mut sql = match term {
2996 OrderTerm::Field { .. } => postrust_sql::escape_ident(&field.name),
2997 OrderTerm::Relation { relation, .. } => {
2998 let (plan, grandchild_alias, grandchild_qi) =
2999 related_order_target(schema_cache, child_qi, child_select, relation, ctx)?;
3000
3001 // The column belongs to the related table, so its type is read
3002 // from there rather than from the child.
3003 let pg_type = schema_cache
3004 .get_table(&grandchild_qi)
3005 .and_then(|table| table.get_column(&field.name))
3006 .map(|column| column.data_type.clone())
3007 .unwrap_or_default();
3008 let coercible = postrust_core::plan::CoercibleField::from_field(field, &pg_type);
3009 let column = postrust_core::query::QueryBuilder::qualified_column_sql(
3010 Some(&grandchild_alias),
3011 &coercible,
3012 );
3013
3014 // The child is a real table alias at this level, so its alias
3015 // is also the expression for its whole row.
3016 plan.order_expression(child_alias, child_alias, &grandchild_alias, &column)
3017 }
3018 };
3019
3020 match direction {
3021 Some(OrderDirection::Desc) => sql.push_str(" DESC"),
3022 Some(OrderDirection::Asc) => sql.push_str(" ASC"),
3023 None => {}
3024 }
3025 match nulls {
3026 Some(OrderNulls::First) => sql.push_str(" NULLS FIRST"),
3027 Some(OrderNulls::Last) => sql.push_str(" NULLS LAST"),
3028 None => {}
3029 }
3030
3031 rendered.push(sql);
3032 }
3033
3034 Ok(Some(rendered.join(", ")))
3035}
3036
3037#[allow(clippy::too_many_arguments)] // each names one part of where the embed sits
3038fn build_embed_expressions(
3039 schema_cache: &postrust_core::SchemaCache,
3040 parent_qi: &postrust_core::api_request::QualifiedIdentifier,
3041 parent_alias: &str,
3042 // The SQL expression yielding the parent's whole row, which a computed
3043 // relationship takes as its argument. At a nested level the parent is a
3044 // real table alias and so is the row; at the top level the parent is a
3045 // derived table, whose alias is a `record` rather than the table's own
3046 // composite type, so a column carrying the row is passed instead.
3047 parent_row: &str,
3048 select: &[postrust_core::api_request::SelectItem],
3049 ctx: &mut EmbedFilters<'_>,
3050 path: &[String],
3051 // The same path spelled with each relation's own name rather than the
3052 // alias the request gave it.
3053 names: &[String],
3054) -> Result<EmbedLevel, postrust_core::Error> {
3055 use postrust_core::api_request::SelectItem;
3056
3057 let mut level = EmbedLevel::default();
3058
3059 for item in select {
3060 // A spread is embedded exactly like a plain relation. The two differ
3061 // only in where the child's columns end up, and that is settled once
3062 // the rows are JSON -- so here the spread is marked by the name its
3063 // expression is given, and flattened afterwards.
3064 let (relation, alias, child_select, join_type, hint, is_spread) = match item {
3065 SelectItem::Relation {
3066 relation,
3067 alias,
3068 select,
3069 join_type,
3070 hint,
3071 } => (relation, alias.clone(), select, join_type, hint, false),
3072 SelectItem::SpreadRelation {
3073 relation,
3074 select,
3075 join_type,
3076 hint,
3077 } => (relation, None, select, join_type, hint, true),
3078 SelectItem::Field { .. } => continue,
3079 };
3080
3081 level.saw_relations = true;
3082
3083 let rel = schema_cache
3084 .find_relationship(parent_qi, relation, hint.as_deref(), &parent_qi.schema)?
3085 .ok_or_else(|| {
3086 schema_cache.relationship_not_found(
3087 parent_qi,
3088 relation,
3089 hint.as_deref(),
3090 &parent_qi.schema,
3091 )
3092 })?;
3093 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
3094
3095 // No row to pass: the caller said the parent's is not expressible
3096 // here, and a computed relationship is a function of it.
3097 if plan.function.is_some() && parent_row.is_empty() {
3098 continue;
3099 }
3100
3101 let child_alias = ctx.next_alias();
3102 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
3103 &plan.foreign_schema,
3104 &plan.foreign_table,
3105 );
3106
3107 // Filters are addressed by the name the client used, which may be
3108 // either the alias or the relation itself: `c:clients(*)&c.id=eq.1`
3109 // and `the_tasks:tasks(*)&tasks.id=eq.1` both name the same embed.
3110 let mut child_path = path.to_vec();
3111 child_path.push(alias.clone().unwrap_or_else(|| relation.clone()));
3112 let mut child_names = names.to_vec();
3113 child_names.push(relation.clone());
3114 let child_where = ctx.predicate_for(&child_path, &child_names, &child_qi, schema_cache)?;
3115
3116 // Deeper relations first: they become part of this level's SELECT list.
3117 let child_row = postrust_sql::escape_ident(&child_alias);
3118 let nested = build_embed_expressions(
3119 schema_cache,
3120 &child_qi,
3121 &child_alias,
3122 &child_row,
3123 child_select,
3124 ctx,
3125 &child_path,
3126 &child_names,
3127 )?;
3128
3129 // A nested `!inner` is written against this child's alias, which is
3130 // only in scope inside this child's own subselect -- so it narrows the
3131 // child here rather than travelling up to the top-level wrapper.
3132 let child_where = match (child_where, nested.inner_joins.is_empty()) {
3133 (existing, true) => existing,
3134 (Some(existing), false) => Some(format!(
3135 "{} AND {}",
3136 existing,
3137 nested.inner_joins.join(" AND ")
3138 )),
3139 (None, false) => Some(nested.inner_joins.join(" AND ")),
3140 };
3141
3142 // `!inner` on this relation restricts the parent rows. It is emitted
3143 // against `parent_alias`, so it belongs to the caller's level; reusing
3144 // the same predicate string lets both places share placeholders.
3145 // The child keeps the same alias it has in the embed subselect. The
3146 // two are sibling scopes, never nested, so there is no collision --
3147 // and any nested predicate folded into `child_where` already names
3148 // that alias, so it has to be the one in scope here too.
3149 if matches!(join_type, Some(postrust_core::api_request::JoinType::Inner)) {
3150 level.inner_joins.push(plan.inner_join_predicate(
3151 parent_alias,
3152 parent_row,
3153 &child_alias,
3154 child_where.as_deref(),
3155 ));
3156 }
3157
3158 level.spread_columns.extend(nested.spread_columns);
3159 let nested_expressions = nested.expressions;
3160
3161 let child_table =
3162 schema_cache.get_table(&postrust_core::api_request::QualifiedIdentifier::new(
3163 &plan.foreign_schema,
3164 &plan.foreign_table,
3165 ));
3166
3167 // The child's own columns. Empty means every column, which is what a
3168 // relation with no explicit selection asks for.
3169 let mut parts: Vec<String> = Vec::new();
3170 let mut project_everything = child_select.is_empty();
3171 for nested_item in child_select {
3172 match nested_item {
3173 SelectItem::Field { field, .. } if field.name == "*" => {
3174 project_everything = true;
3175 }
3176 SelectItem::Field { field, alias, .. } => {
3177 let column = postrust_sql::escape_ident(&field.name);
3178 // A schema that declared how one of its domains is written
3179 // on the wire meant it wherever the value appears, and an
3180 // embedded row is the same value in a different place.
3181 let rendered = child_table
3182 .as_ref()
3183 .and_then(|table| table.get_column(&field.name))
3184 .and_then(|c| schema_cache.representation(c.representation_type(), "json"))
3185 .map(|function| format!("{}({})", function, column))
3186 .unwrap_or_else(|| column.clone());
3187 let name = alias.as_deref().unwrap_or(&field.name);
3188 parts.push(format!(
3189 "{} AS {}",
3190 rendered,
3191 postrust_sql::escape_ident(name)
3192 ));
3193 }
3194 // Handled as an expression, not a column.
3195 SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. } => {}
3196 }
3197 }
3198 if project_everything {
3199 parts.clear();
3200 parts.push(format!("{}.*", postrust_sql::escape_ident(&child_alias)));
3201 }
3202 for (field_name, expression) in nested_expressions {
3203 parts.push(format!(
3204 "{} AS {}",
3205 expression,
3206 postrust_sql::escape_ident(&field_name)
3207 ));
3208 }
3209
3210 let inner_select = parts.join(", ");
3211 let child_order = embed_order_sql(
3212 schema_cache,
3213 &child_qi,
3214 child_select,
3215 &child_alias,
3216 ctx,
3217 &child_path,
3218 &child_names,
3219 )?;
3220 let (child_limit, child_offset) = ctx.range_for(&child_path, &child_names);
3221 let expression = plan.embed_expression(
3222 parent_alias,
3223 parent_row,
3224 &child_alias,
3225 &inner_select,
3226 child_limit,
3227 child_offset,
3228 child_where.as_deref(),
3229 child_order.as_deref(),
3230 // Positionally, with the parent row and nothing else: this surface
3231 // has no session to supply and no place to write an argument.
3232 None,
3233 ctx.rendering,
3234 )?;
3235
3236 if is_spread {
3237 let names = spread_output_names(schema_cache, &child_qi, child_select);
3238 if names.is_empty() {
3239 // `...clients()` spreads no columns, so there is nothing to
3240 // fetch and nothing to merge.
3241 continue;
3242 }
3243 level
3244 .spread_columns
3245 .push((format!("{}{}", SPREAD_KEY_PREFIX, child_alias), names));
3246 }
3247
3248 let key = match is_spread {
3249 // A name nothing can collide with, since the object it names is
3250 // dissolved into its parent before anyone sees it.
3251 true => format!("{}{}", SPREAD_KEY_PREFIX, child_alias),
3252 false => alias.clone().unwrap_or_else(|| relation.clone()),
3253 };
3254
3255 // Under the name the client used, whatever the response key is: a
3256 // filter naming the embed is written against that name.
3257 //
3258 // What it records is an existence test, not the expression. Asking
3259 // whether a to-many embed is null would never be true -- it renders as
3260 // `[]`, never as null -- and "did this match anything" is the question
3261 // either way.
3262 level.filterable.push((
3263 alias.clone().unwrap_or_else(|| relation.clone()),
3264 plan.inner_join_predicate(
3265 parent_alias,
3266 parent_row,
3267 &child_alias,
3268 child_where.as_deref(),
3269 ),
3270 ));
3271
3272 // `clients()` asks for none of the related resource's columns. It is
3273 // written to narrow the parent -- with `!inner`, or with a filter on
3274 // the embed -- and the resource itself is not part of the answer, so
3275 // it gets no key at all rather than an empty object. The test is
3276 // recursive: an embed whose only content is such an embed contributes
3277 // nothing either.
3278 if !yields_columns(child_select) {
3279 continue;
3280 }
3281
3282 level.expressions.push((key, expression));
3283 }
3284
3285 Ok(level)
3286}
3287
3288/// The media type a function's own return type declares, where the client
3289/// asked for it.
3290///
3291/// A function returning a domain named `text/plain` returns text, and the
3292/// value is the whole response. It is still only returned that way to a client
3293/// that asked: the same value serialised as JSON is a perfectly good answer to
3294/// a request that did not.
3295fn declared_media_type(
3296 db_plan: &DbActionPlan,
3297 api_request: &postrust_core::api_request::ApiRequest,
3298) -> Option<(String, String)> {
3299 let DbActionPlan::Call { call, .. } = db_plan else {
3300 return None;
3301 };
3302 let declared = call.media_type.as_deref()?;
3303 let base = call.media_base_type.clone().unwrap_or_default();
3304 // A function declaring `*/*` says it will render whatever was asked for,
3305 // so every request is one it answers -- and what it answers with is bytes,
3306 // which on the wire is called `application/octet-stream`.
3307 if declared == "*/*" {
3308 return Some(("application/octet-stream".to_string(), base));
3309 }
3310 api_request
3311 .accept_media_types
3312 .iter()
3313 .any(|accepted| accepted.content_type() == declared)
3314 .then(|| (declared.to_string(), base))
3315}
3316
3317/// Whether a selection produces any keys in the response.
3318///
3319/// A relation contributes only what its own selection does, so `tasks()` and
3320/// `tasks(projects())` are alike empty, while `tasks(name)` is not.
3321fn yields_columns(select: &[postrust_core::api_request::SelectItem]) -> bool {
3322 use postrust_core::api_request::SelectItem;
3323
3324 select.iter().any(|item| match item {
3325 SelectItem::Field { .. } => true,
3326 SelectItem::Relation { select, .. } | SelectItem::SpreadRelation { select, .. } => {
3327 yields_columns(select)
3328 }
3329 })
3330}
3331
3332type EmbedFuture<'f> = std::pin::Pin<
3333 Box<dyn std::future::Future<Output = Result<(), postrust_core::Error>> + Send + 'f>,
3334>;
3335
3336/// Attach the relations requested in `select` onto `rows`.
3337///
3338/// One query per relation per level: the parents' join keys are collected and
3339/// passed as a single array, so embedding across a page of parents does not
3340/// become a query per row. Recurses for nested embeds.
3341fn embed_relations<'f>(
3342 conn: &'f mut sqlx::PgConnection,
3343 schema_cache: &'f postrust_core::SchemaCache,
3344 parent_qi: &'f postrust_core::api_request::QualifiedIdentifier,
3345 select: &'f [postrust_core::api_request::SelectItem],
3346 rows: &'f mut [serde_json::Value],
3347 max_rows: Option<i64>,
3348) -> EmbedFuture<'f> {
3349 use postrust_core::api_request::SelectItem;
3350
3351 Box::pin(async move {
3352 if rows.is_empty() {
3353 return Ok(());
3354 }
3355
3356 for item in select {
3357 // A spread embed is fetched exactly like a plain one -- the two
3358 // differ only in where the child's columns end up, which is
3359 // settled once the rows are in hand.
3360 let (relation, alias, nested, is_spread, hint) = match item {
3361 SelectItem::Relation {
3362 relation,
3363 alias,
3364 select: nested,
3365 hint,
3366 ..
3367 } => (relation, alias.clone(), nested, false, hint),
3368 SelectItem::SpreadRelation {
3369 relation,
3370 select: nested,
3371 hint,
3372 ..
3373 } => (relation, None, nested, true, hint),
3374 SelectItem::Field { .. } => continue,
3375 };
3376
3377 let rel = schema_cache
3378 .find_relationship(parent_qi, relation, hint.as_deref(), &parent_qi.schema)?
3379 .ok_or_else(|| {
3380 schema_cache.relationship_not_found(
3381 parent_qi,
3382 relation,
3383 hint.as_deref(),
3384 &parent_qi.schema,
3385 )
3386 })?;
3387
3388 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
3389 let keys = postrust_core::embed::parent_keys(rows, &plan.local_column);
3390
3391 // Which of the child's columns the query needs to return.
3392 //
3393 // A column that was not asked for still costs a heap read, JSON
3394 // serialisation in PostgreSQL, socket bytes and a parse before
3395 // being discarded, so the projection is worked out before the query
3396 // rather than filtered afterwards.
3397 //
3398 // A nested embed joins on a column of the child row, so that column
3399 // has to survive the projection even when the client did not ask
3400 // for it. A spread relation falls back to every column: its shape
3401 // is decided further down and is not a plain projection.
3402 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
3403 &plan.foreign_schema,
3404 &plan.foreign_table,
3405 );
3406
3407 let mut child_columns: Vec<String> = Vec::new();
3408 let mut project_everything = nested.is_empty();
3409 for nested_item in nested {
3410 match nested_item {
3411 SelectItem::Field { field, .. } => child_columns.push(field.name.clone()),
3412 SelectItem::Relation { relation, .. } => {
3413 match schema_cache
3414 .find_relationship(&child_qi, relation, None, &child_qi.schema)
3415 .ok()
3416 .flatten()
3417 {
3418 Some(nested_rel) => {
3419 let nested_plan = postrust_core::embed::EmbedPlan::resolve(
3420 nested_rel,
3421 schema_cache,
3422 )?;
3423 child_columns.push(nested_plan.local_column);
3424 }
3425 // Leave it to the recursive call to report the
3426 // unknown relation, rather than failing here with
3427 // less context.
3428 None => project_everything = true,
3429 }
3430 }
3431 SelectItem::SpreadRelation { .. } => project_everything = true,
3432 }
3433 }
3434 if project_everything {
3435 child_columns.clear();
3436 }
3437
3438 let mut grouped = if keys.is_empty() {
3439 std::collections::HashMap::new()
3440 } else {
3441 let sql = plan.children_grouped_sql(max_rows, &child_columns)?;
3442
3443 let fetched = sqlx::query(&sql)
3444 .bind(&keys)
3445 .fetch_all(&mut *conn)
3446 .await
3447 .map_err(map_sqlx_error)?;
3448
3449 // The query returns the join key and a JSON array of that
3450 // key's children, so the values are read straight out of the
3451 // columns -- the typed row converter would wrap them under the
3452 // expression name.
3453 use sqlx::Row;
3454 let pairs: Vec<(serde_json::Value, serde_json::Value)> = fetched
3455 .into_iter()
3456 .filter_map(|row| {
3457 Some((
3458 row.try_get::<serde_json::Value, _>(0).ok()?,
3459 row.try_get::<serde_json::Value, _>(1).ok()?,
3460 ))
3461 })
3462 .collect();
3463
3464 postrust_core::embed::group_from_aggregated(pairs)
3465 };
3466
3467 // Deeper embeds, if any were asked for.
3468 //
3469 // A nested embed issues one query for every child row at this
3470 // level, so the rows have to be contiguous for it. They arrive
3471 // grouped, so they are flattened for the recursive call and put
3472 // back afterwards. When nothing deeper was requested this is all
3473 // skipped, which is the common case.
3474 let has_deeper_embed = nested.iter().any(|item| {
3475 matches!(
3476 item,
3477 SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. }
3478 )
3479 });
3480
3481 if has_deeper_embed {
3482 let mut order: Vec<(String, usize)> = Vec::with_capacity(grouped.len());
3483 let mut flat: Vec<serde_json::Value> = Vec::new();
3484 for (key, children) in grouped.drain() {
3485 order.push((key, children.len()));
3486 flat.extend(children);
3487 }
3488
3489 embed_relations(
3490 &mut *conn,
3491 schema_cache,
3492 &child_qi,
3493 nested,
3494 &mut flat,
3495 max_rows,
3496 )
3497 .await?;
3498
3499 let mut rest = flat.into_iter();
3500 for (key, count) in order {
3501 grouped.insert(key, rest.by_ref().take(count).collect());
3502 }
3503 }
3504
3505 // Return only the requested columns of the related resource. An
3506 // empty nested select means every column.
3507 //
3508 // The projection in the query covers the columns; this covers what
3509 // is left over, which is the join column when the client did not
3510 // ask for it.
3511 // A spread picks its own columns out when it merges them, and does
3512 // it by the child's own column names, so pruning here would only
3513 // get in the way.
3514 let requested: Option<std::collections::HashSet<String>> =
3515 if nested.is_empty() || is_spread {
3516 None
3517 } else {
3518 Some(
3519 nested
3520 .iter()
3521 .flat_map(|nested_item| match nested_item {
3522 SelectItem::Field { field, alias, .. } => {
3523 vec![alias.clone().unwrap_or_else(|| field.name.clone())]
3524 }
3525 SelectItem::Relation {
3526 relation, alias, ..
3527 } => vec![alias.clone().unwrap_or_else(|| relation.clone())],
3528 // A spread one level down has already merged
3529 // its columns into these rows, so they are
3530 // part of what was asked for.
3531 SelectItem::SpreadRelation { select, .. } => select
3532 .iter()
3533 .filter_map(|column| match column {
3534 SelectItem::Field { field, .. } => Some(field.name.clone()),
3535 _ => None,
3536 })
3537 .collect(),
3538 })
3539 .collect(),
3540 )
3541 };
3542
3543 if let Some(requested) = requested {
3544 for group in grouped.values_mut() {
3545 for child in group.iter_mut() {
3546 if let Some(object) = child.as_object_mut() {
3547 object.retain(|key, _| requested.contains(key));
3548 }
3549 }
3550 }
3551 }
3552 if is_spread {
3553 // The child rows carry the table's own column names, so a
3554 // column is read by its real name and written under its alias.
3555 let spread_columns: Vec<(String, String)> = nested
3556 .iter()
3557 .filter_map(|nested_item| match nested_item {
3558 SelectItem::Field { field, alias, .. } => Some((
3559 field.name.clone(),
3560 alias.clone().unwrap_or_else(|| field.name.clone()),
3561 )),
3562 _ => None,
3563 })
3564 .collect();
3565 for row in rows.iter_mut() {
3566 postrust_core::embed::spread_into_parent(row, &plan, &grouped, &spread_columns);
3567 }
3568 } else {
3569 let field_name = alias.clone().unwrap_or_else(|| relation.clone());
3570 for row in rows.iter_mut() {
3571 postrust_core::embed::attach_to_parent(row, &field_name, &plan, &grouped);
3572 }
3573 }
3574 }
3575
3576 Ok(())
3577 })
3578}
3579
3580/// Bind SqlParam values to a sqlx query.
3581fn bind_params<'q>(
3582 mut query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
3583 params: &'q [postrust_sql::SqlParam],
3584) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
3585 use postrust_sql::SqlParam;
3586
3587 for param in params {
3588 query = match param {
3589 SqlParam::Null => query.bind(None::<String>),
3590 SqlParam::Bool(b) => query.bind(b),
3591 SqlParam::Int(n) => query.bind(n),
3592 SqlParam::Float(f) => query.bind(f),
3593 SqlParam::Text(s) => query.bind(s),
3594 SqlParam::Bytes(b) => query.bind(b),
3595 SqlParam::Json(j) => query.bind(j),
3596 SqlParam::Uuid(u) => query.bind(u),
3597 SqlParam::Timestamp(t) => query.bind(t),
3598 SqlParam::Array(arr) => {
3599 // Convert array to Vec<String> for text arrays
3600 let strings: Vec<String> = arr
3601 .iter()
3602 .map(|p| match p {
3603 SqlParam::Text(s) => s.clone(),
3604 SqlParam::Int(n) => n.to_string(),
3605 SqlParam::Bool(b) => b.to_string(),
3606 other => format!("{:?}", other),
3607 })
3608 .collect();
3609 query.bind(strings)
3610 }
3611 };
3612 }
3613
3614 query
3615}
3616
3617/// Map sqlx error to our error type.
3618fn map_sqlx_error(e: sqlx::Error) -> postrust_core::Error {
3619 match e {
3620 sqlx::Error::Database(db_err) => {
3621 // Try to downcast to Postgres-specific error for additional details
3622 let (details, hint) = db_err
3623 .try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
3624 .map(|pg_err| {
3625 (
3626 pg_err.detail().map(String::from),
3627 pg_err.hint().map(String::from),
3628 )
3629 })
3630 .unwrap_or((None, None));
3631
3632 postrust_core::Error::Database(postrust_core::error::DatabaseError {
3633 code: db_err.code().map(|c| c.to_string()).unwrap_or_default(),
3634 message: db_err.message().to_string(),
3635 details,
3636 hint,
3637 constraint: db_err.constraint().map(|s| s.to_string()),
3638 table: db_err.table().map(|s| s.to_string()),
3639 column: None,
3640 })
3641 }
3642 other => postrust_core::Error::Internal(other.to_string()),
3643 }
3644}
3645
3646/// Build an HTTP response from our response type.
3647fn build_response(response: PgrstResponse) -> Response {
3648 let mut builder = Response::builder().status(response.status);
3649
3650 for (key, value) in &response.headers {
3651 builder = builder.header(key, value);
3652 }
3653
3654 builder
3655 .body(Body::from(response.body))
3656 .unwrap_or_else(|_| Response::new(Body::empty()))
3657}
3658
3659/// Build an error response.
3660///
3661/// In production mode (PGRST_DEBUG=false or unset), sensitive error details
3662/// are hidden to prevent information leakage.
3663/// Build the response for a failed request.
3664///
3665/// `verbatim_db_errors` passes a database failure through as PostgreSQL
3666/// reported it -- its SQLSTATE, message, detail and hint -- which is what
3667/// PostgREST does and what a client written against PostgREST branches on.
3668/// It is off unless compatibility mode is on, because those fields describe
3669/// the schema rather than the request: a constraint's name, a column that was
3670/// not selected, the text of a failing query. Everything else is reported the
3671/// same either way.
3672fn error_response(error: postrust_core::Error, verbatim_db_errors: bool) -> Response {
3673 let status = error.status_code();
3674
3675 // Check if debug mode is enabled
3676 let debug_mode = std::env::var("PGRST_DEBUG")
3677 .map(|v| v == "true" || v == "1")
3678 .unwrap_or(false);
3679
3680 // A function that raised `sqlstate 'PGRST'` supplied the whole response:
3681 // its status, its headers and its body. Nothing here is the API layer's to
3682 // decide, including whether to sanitise it -- the schema wrote it.
3683 if let postrust_core::Error::Database(db) = &error {
3684 match db.raised_response() {
3685 Some(Ok(raised)) => {
3686 let mut builder = Response::builder()
3687 .status(status)
3688 .header("content-type", "application/json");
3689 for (name, value) in &raised.headers {
3690 builder = builder.header(name, value);
3691 }
3692 return builder
3693 .body(Body::from(
3694 serde_json::to_vec(&raised.body).unwrap_or_default(),
3695 ))
3696 .unwrap_or_else(|_| Response::new(Body::empty()));
3697 }
3698 // The schema meant to write a response and wrote something else.
3699 // Reported as the fault it is, and reported the same way whether
3700 // or not database errors are passed through, since this one is
3701 // about the schema's own text rather than about the request.
3702 Some(Err(fault)) => {
3703 return error_response(
3704 postrust_core::Error::RaiseNotUnderstood(fault),
3705 verbatim_db_errors,
3706 )
3707 }
3708 None => {}
3709 }
3710 }
3711
3712 let body = if debug_mode {
3713 // Full error details in debug mode
3714 serde_json::to_vec(&error.to_json()).unwrap_or_default()
3715 } else if let (true, postrust_core::Error::Database(db)) = (verbatim_db_errors, &error) {
3716 serde_json::to_vec(&serde_json::json!({
3717 "code": db.code,
3718 "message": db.message,
3719 "details": db.details,
3720 "hint": db.hint,
3721 }))
3722 .unwrap_or_default()
3723 } else {
3724 // Sanitized error in production
3725 let sanitized = serde_json::json!({
3726 "code": error.code(),
3727 "message": sanitize_error_message(&error),
3728 "details": error.details(),
3729 // A hint says how to correct the request. It is carried by the
3730 // error itself, so it never has to reach for anything the
3731 // sanitised message deliberately left out.
3732 "hint": error.hint(),
3733 });
3734 serde_json::to_vec(&sanitized).unwrap_or_default()
3735 };
3736
3737 let mut builder = Response::builder()
3738 .status(status)
3739 .header("content-type", "application/json");
3740
3741 // A 401 has to say what would satisfy it, or a client has no way to know
3742 // it should be sending a token at all. Where a token *was* sent and was
3743 // not accepted, the challenge says which -- `invalid_token` with the
3744 // reason, so a client can tell "your key is wrong" from "your clock is"
3745 // without parsing the body.
3746 if status == StatusCode::UNAUTHORIZED {
3747 let challenge = match &error {
3748 postrust_core::Error::InvalidJwt(_)
3749 | postrust_core::Error::NoSuitableJwtKey
3750 | postrust_core::Error::JwtClaim(_) => format!(
3751 "Bearer error=\"invalid_token\", error_description=\"{}\"",
3752 error
3753 ),
3754 _ => "Bearer".to_string(),
3755 };
3756 builder = builder.header("www-authenticate", challenge);
3757 }
3758
3759 builder
3760 .body(Body::from(body))
3761 .unwrap_or_else(|_| Response::new(Body::empty()))
3762}
3763
3764/// Sanitize error messages for production.
3765fn sanitize_error_message(error: &postrust_core::Error) -> String {
3766 use postrust_core::Error;
3767
3768 // A schema-cache miss names what the client asked for, and the client
3769 // already knows that -- it is what it sent. Saying so is what makes the
3770 // difference between "Resource not found" and a message the client can act
3771 // on, and it discloses nothing it did not supply. What stays sanitised is
3772 // the database's own account of a failure, which is about the schema
3773 // rather than about the request.
3774 match error {
3775 Error::TableNotFound { name, .. } | Error::NotFound(name) => {
3776 return format!("Could not find the table '{}' in the schema cache", name)
3777 }
3778 Error::FunctionNotFound {
3779 name,
3780 params,
3781 single_param,
3782 ..
3783 } => {
3784 // A body that is one value carries no argument names, so there is
3785 // no argument list to report -- naming the function is the whole
3786 // of what was looked for.
3787 let looked_for =
3788 match postrust_core::error::names_only_one_param(single_param.as_deref()) {
3789 true => name.clone(),
3790 false => postrust_core::error::function_signature(name, params),
3791 };
3792 return format!(
3793 "Could not find the function {} in the schema cache",
3794 looked_for
3795 );
3796 }
3797 Error::UnacceptableSchema { requested, .. } => {
3798 return format!("Invalid schema: {}", requested)
3799 }
3800 // The candidate list is the whole point of the message: it names the
3801 // signatures that could not be told apart.
3802 Error::AmbiguousFunction { .. } | Error::AmbiguousRelationship { .. } => {
3803 // The candidates are the whole point of these messages: they name
3804 // what could not be told apart.
3805 return error.to_string();
3806 }
3807 _ => {}
3808 }
3809
3810 (match error {
3811 Error::ColumnNotFound(_) => "Column not found",
3812 // The column is the client's own word and the relation is the one it
3813 // addressed, so naming both says nothing it did not send.
3814 Error::UnknownColumn { .. } => return error.to_string(),
3815 // Each of these says only what the request itself said: the resource
3816 // it named, the preference it sent, the shape it asked for. Repeating
3817 // that back tells the client nothing it did not already know, and is
3818 // the difference between a message it can act on and one it cannot.
3819 Error::RelationshipNotFound { .. }
3820 | Error::NotAnEmbeddedResource(_)
3821 | Error::RelatedOrderNotPossible { .. }
3822 | Error::InvalidPreferences(_)
3823 | Error::NotSingular { .. }
3824 | Error::InvalidRange(_)
3825 | Error::InvalidBody(_)
3826 | Error::UnsupportedMethod(_)
3827 | Error::InvalidRpcMethod(_)
3828 | Error::InvalidPutFilters
3829 | Error::PutLimitNotAllowed
3830 | Error::PutMatchingPk
3831 | Error::MaxAffectedExceeded(_)
3832 | Error::InvalidGucHeaders
3833 | Error::InvalidGucStatus
3834 | Error::InvalidMediaType(_)
3835 // Says only that the rows the request asked for carry no geometry,
3836 // which is about the request's own shape and is what the client has
3837 // to change.
3838 | Error::MissingGeometryColumn
3839 // Quotes the client's own URL back at it, and names the grammar the
3840 // API publishes. Neither is the schema's.
3841 | Error::UnparsableQuery { .. }
3842 // Says nothing about the schema beyond that one RAISE is malformed,
3843 // which is what the author has to fix.
3844 | Error::RaiseNotUnderstood(_)
3845 | Error::InvalidResourcePath => return error.to_string(),
3846 Error::InvalidPath(_) => "Invalid request path",
3847 // What the token failed on is the client's own token, so naming it
3848 // costs nothing and is the only way it can tell a bad signature from
3849 // an expired one.
3850 Error::InvalidJwt(_)
3851 | Error::NoSuitableJwtKey
3852 | Error::JwtClaim(_)
3853 | Error::MissingAuth => return error.to_string(),
3854 Error::InsufficientPermissions(_) => "Forbidden",
3855
3856 // A fixed policy message that reveals nothing about the request or the
3857 // schema, so it is passed through verbatim -- and it is what PostgREST
3858 // says, which is what a client branching on it will be matching.
3859 Error::AggregatesNotAllowed => "Use of aggregate functions is not allowed",
3860 Error::InvalidHeader(_) | Error::InvalidQueryParam(_) => "Invalid request",
3861 Error::Database(_) => "Database error",
3862 Error::ConnectionPool(_) => "Service temporarily unavailable",
3863 Error::Internal(_) => "Internal server error",
3864 _ => "An error occurred",
3865 })
3866 .to_string()
3867}
3868
3869#[cfg(test)]
3870mod tests {
3871 /// The REST surface is mounted at `/api`, and an `OPTIONS` reaches the
3872 /// layer before the mount is stripped off.
3873 #[test]
3874 fn the_api_path_is_the_one_below_the_mount() {
3875 assert_eq!(super::api_path("/api/items", false), Some("/items"));
3876 assert_eq!(super::api_path("/api/rpc/f", false), Some("/rpc/f"));
3877 assert_eq!(super::api_path("/api", false), Some("/"));
3878 assert_eq!(super::api_path("/api/", false), Some("/"));
3879 }
3880
3881 /// Answering for a route that is not the API's turns a working preflight
3882 /// into a 404 for a table nobody asked about.
3883 #[test]
3884 fn a_route_that_is_not_the_api_is_left_alone() {
3885 for path in [
3886 "/admin",
3887 "/admin/swagger",
3888 "/_",
3889 "/_/health",
3890 "/api/graphql",
3891 // The endpoints a Hasura deployment has. `/v1/graphql` is the one
3892 // every generated client is pointed at, so answering its preflight
3893 // with "no such table" breaks the surface this branch exists to
3894 // offer -- and only in compatibility mode, where nothing else
3895 // looks wrong.
3896 "/healthz",
3897 "/v1/version",
3898 "/v1/graphql",
3899 "/v1/graphql/ws",
3900 "/v1alpha1/graphql",
3901 ] {
3902 assert_eq!(super::api_path(path, false), None, "{path}");
3903 assert_eq!(super::api_path(path, true), None, "{path}");
3904 }
3905 }
3906
3907 /// The mounts are whole segments here too: a table may be called `v1`.
3908 #[test]
3909 fn a_hasura_mount_does_not_swallow_a_table_beside_it() {
3910 assert_eq!(super::api_path("/v1", true), Some("/v1"));
3911 assert_eq!(super::api_path("/healthzz", true), Some("/healthzz"));
3912 assert_eq!(super::api_path("/v1alpha1", true), Some("/v1alpha1"));
3913 }
3914
3915 /// Outside the mounts the surface exists only in compatibility mode,
3916 /// where PostgREST's own paths answer at the root.
3917 #[test]
3918 fn the_root_is_the_api_only_in_compatibility_mode() {
3919 assert_eq!(super::api_path("/items", true), Some("/items"));
3920 assert_eq!(super::api_path("/", true), Some("/"));
3921 assert_eq!(super::api_path("/items", false), None);
3922 assert_eq!(super::api_path("/", false), None);
3923 }
3924
3925 /// A mount is a path segment: `/apiary` is not under `/api`.
3926 #[test]
3927 fn a_mount_matches_whole_segments_only() {
3928 assert_eq!(super::api_path("/apiary", true), Some("/apiary"));
3929 assert_eq!(
3930 super::api_path("/administrators", true),
3931 Some("/administrators")
3932 );
3933 }
3934
3935 /// A data-modifying `WITH` may only appear at the top level, so anything
3936 /// that wraps the statement has to leave the clause where it is.
3937 #[test]
3938 fn a_leading_cte_is_lifted_off_the_statement() {
3939 let (clause, body) = super::split_leading_cte(
3940 "WITH pgrst_mutation_result AS (DELETE FROM t WHERE n = ')' RETURNING t.id) \
3941 SELECT \"id\" FROM \"pgrst_mutation_result\"",
3942 );
3943
3944 assert_eq!(
3945 clause,
3946 "WITH pgrst_mutation_result AS (DELETE FROM t WHERE n = ')' RETURNING t.id) "
3947 );
3948 assert_eq!(body, "SELECT \"id\" FROM \"pgrst_mutation_result\"");
3949 }
3950
3951 /// A plain read has no clause to lift.
3952 #[test]
3953 fn a_statement_without_a_cte_is_left_alone() {
3954 let sql = "SELECT \"id\" FROM \"test\".\"items\"";
3955 let (clause, body) = super::split_leading_cte(sql);
3956
3957 assert!(clause.is_empty());
3958 assert_eq!(body, sql);
3959 }
3960
3961 use super::*;
3962 use postrust_core::plan::CallParams;
3963 use postrust_core::QualifiedIdentifier;
3964 use serde_json::json;
3965
3966 fn call_plan(name: &str, returns_set: bool) -> CallPlan {
3967 CallPlan {
3968 output_columns: Vec::new(),
3969 media_type: None,
3970 media_base_type: None,
3971 function: QualifiedIdentifier::new("public", name),
3972 params: CallParams::None,
3973 returns_scalar: !returns_set,
3974 return_type: None,
3975 returns_set,
3976 returns_composite: false,
3977 volatility: "Volatile".into(),
3978 param_types: Vec::new(),
3979 returns_void: false,
3980 variadic_params: Vec::new(),
3981 }
3982 }
3983
3984 fn composite_call_plan(name: &str, returns_set: bool) -> CallPlan {
3985 CallPlan {
3986 returns_composite: true,
3987 returns_scalar: false,
3988 return_type: None,
3989 ..call_plan(name, returns_set)
3990 }
3991 }
3992
3993 #[test]
3994 fn unwraps_json_return_to_bare_object() {
3995 // `SELECT * FROM sync(...)` on a json-returning function yields a single
3996 // column named after the function.
3997 let rows = vec![json!({"sync": {"ok": true, "count": 3}})];
3998 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("sync", false));
3999 assert!(singular, "non-set-returning function should be singular");
4000 assert_eq!(rows, vec![json!({"ok": true, "count": 3})]);
4001 }
4002
4003 #[test]
4004 fn unwraps_scalar_return() {
4005 let rows = vec![json!({"add": 42})];
4006 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("add", false));
4007 assert!(singular);
4008 assert_eq!(rows, vec![json!(42)]);
4009 }
4010
4011 #[test]
4012 fn unwraps_setof_scalar_to_array() {
4013 let rows = vec![json!({"gen": 1}), json!({"gen": 2})];
4014 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("gen", true));
4015 assert!(!singular, "set-returning function should not be singular");
4016 assert_eq!(rows, vec![json!(1), json!(2)]);
4017 }
4018
4019 #[test]
4020 fn leaves_multi_column_rows_untouched() {
4021 // `RETURNS TABLE(...)` / composite set: rows already have real columns
4022 // and must not be un-nested.
4023 let rows = vec![json!({"id": 1, "name": "a"}), json!({"id": 2, "name": "b"})];
4024 let (out, singular) =
4025 unwrap_rpc_rows(rows.clone(), &composite_call_plan("list_users", true));
4026 assert!(!singular);
4027 assert_eq!(out, rows);
4028 }
4029
4030 #[test]
4031 fn leaves_table_column_named_like_function_untouched() {
4032 // `CREATE FUNCTION foo() RETURNS TABLE(foo int)`: the single output
4033 // column legitimately shares the function's name. The composite
4034 // return-type metadata must prevent it from being mistaken for the
4035 // function-name wrapper.
4036 let rows = vec![json!({"foo": 1}), json!({"foo": 2})];
4037 let (out, singular) = unwrap_rpc_rows(rows.clone(), &composite_call_plan("foo", true));
4038 assert!(!singular);
4039 assert_eq!(out, rows);
4040 }
4041
4042 #[test]
4043 fn single_composite_return_is_singular_but_not_unwrapped() {
4044 // A non-set function returning a row type expands to its columns;
4045 // nothing to un-nest, but the result still renders as a bare object.
4046 let rows = vec![json!({"id": 1, "name": "a"})];
4047 let (out, singular) =
4048 unwrap_rpc_rows(rows.clone(), &composite_call_plan("get_user", false));
4049 assert!(singular);
4050 assert_eq!(out, rows);
4051 }
4052
4053 #[test]
4054 fn leaves_single_key_row_untouched_when_key_is_not_function_name() {
4055 // A single real column that happens to be the only column should not be
4056 // mistaken for the function-name wrapper.
4057 let rows = vec![json!({"id": 7})];
4058 let (out, _) = unwrap_rpc_rows(rows.clone(), &call_plan("get_thing", false));
4059 assert_eq!(out, rows);
4060 }
4061}