1use 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
18pub async fn handle_request(State(state): State<Arc<AppState>>, request: Request) -> Response {
20 let method = request.method().clone();
21 let path = request.uri().path().to_string();
22
23 debug!("{} {}", method, path);
24
25 match process_request(state, request).await {
26 Ok(response) => response.into_response(),
27 Err(e) => error_response(e).into_response(),
28 }
29}
30
31async fn process_request(
33 state: Arc<AppState>,
34 request: Request,
35) -> Result<Response, postrust_core::Error> {
36 let auth_header = request
38 .headers()
39 .get("authorization")
40 .and_then(|v| v.to_str().ok());
41
42 let auth_result = authenticate(auth_header, &state.jwt_config)
44 .map_err(|e| postrust_core::Error::InvalidJwt(e.to_string()))?;
45
46 debug!("Authenticated as role: {}", auth_result.role);
47
48 let (parts, body) = request.into_parts();
50 let body_bytes = axum::body::to_bytes(body, 10 * 1024 * 1024)
51 .await
52 .map_err(|e| postrust_core::Error::InvalidBody(e.to_string()))?;
53
54 let mut builder = http::Request::builder()
56 .method(parts.method.clone())
57 .uri(parts.uri.clone());
58
59 for (key, value) in &parts.headers {
60 builder = builder.header(key, value);
61 }
62
63 let http_request = builder
64 .body(body_bytes.clone())
65 .map_err(|e| postrust_core::Error::Internal(e.to_string()))?;
66
67 let mut api_request = parse_request(
69 &http_request,
70 state.default_schema(),
71 state.schemas(),
72 state.config.db_max_rows,
73 )?;
74
75 if !body_bytes.is_empty() {
77 let payload = postrust_core::api_request::payload::parse_payload(
78 body_bytes,
79 &api_request.content_media_type,
80 )?;
81 api_request.payload = payload;
82 }
83
84 let schema_cache = state.schema_cache().await;
86
87 let added_join_columns = add_embed_join_columns(&mut api_request, &schema_cache)?;
91
92 let plan = create_action_plan(&api_request, &schema_cache)?;
94
95 let result = execute_plan(
97 &state,
98 &api_request,
99 &plan,
100 &auth_result,
101 &added_join_columns,
102 )
103 .await?;
104
105 let response = format_response(&api_request, &result)
107 .map_err(|e| postrust_core::Error::Internal(e.to_string()))?;
108
109 Ok(build_response(response))
110}
111
112async fn execute_plan(
114 state: &AppState,
115 api_request: &ApiRequest,
116 plan: &ActionPlan,
117 auth: &postrust_auth::AuthResult,
118 added_join_columns: &[String],
119) -> Result<QueryResult, postrust_core::Error> {
120 match plan {
121 ActionPlan::Db(db_plan) => {
122 let query = postrust_core::query::build_query(
124 &ActionPlan::Db(db_plan.clone()),
125 Some(&auth.role),
126 )?;
127
128 if !query.has_main() {
129 return Ok(QueryResult::default());
130 }
131
132 let (mut sql, params) = query.build_main();
133
134 let embed_expressions = match read_target(api_request) {
145 Some(parent_qi)
146 if api_request.query_params.select.iter().any(|item| {
147 matches!(
148 item,
149 postrust_core::api_request::SelectItem::Relation { .. }
150 )
151 }) =>
152 {
153 let schema_cache = state.schema_cache().await;
154 let mut counter = 0;
155 build_embed_expressions(
156 &schema_cache,
157 &parent_qi,
158 "src",
159 &api_request.query_params.select,
160 api_request.max_rows,
161 &mut counter,
162 )?
163 }
164 _ => Vec::new(),
165 };
166
167 if !embed_expressions.is_empty() {
168 let mut projection = String::from("src.*");
169 for (field_name, expression) in &embed_expressions {
170 projection.push_str(", ");
171 projection.push_str(expression);
172 projection.push_str(" AS ");
173 projection.push_str(&postrust_sql::escape_ident(field_name));
174 }
175 sql = format!("SELECT {} FROM ({}) AS src", projection, sql);
176 }
177
178 debug!("Executing SQL: {}", sql);
179 debug!("With {} parameters", params.len());
180
181 let mut tx = state
195 .pool
196 .begin()
197 .await
198 .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
199
200 sqlx::query(&format!(
202 "SET LOCAL ROLE {}",
203 postrust_sql::escape_ident(&auth.role)
204 ))
205 .execute(&mut *tx)
206 .await
207 .map_err(|e| {
208 postrust_core::Error::Database(postrust_core::error::DatabaseError {
209 code: "42501".into(),
210 message: e.to_string(),
211 details: None,
212 hint: None,
213 constraint: None,
214 table: None,
215 column: None,
216 })
217 })?;
218
219 for (key, value) in &auth.claims {
221 let guc_key = format!("request.jwt.claims.{}", key);
222 let guc_value = match value {
223 serde_json::Value::String(s) => s.clone(),
224 other => other.to_string(),
225 };
226
227 sqlx::query("SELECT set_config($1, $2, true)")
228 .bind(&guc_key)
229 .bind(&guc_value)
230 .execute(&mut *tx)
231 .await
232 .ok(); }
234
235 let rows = bind_params(sqlx::query(&sql), ¶ms)
237 .fetch_all(&mut *tx)
238 .await
239 .map_err(|e| {
240 error!("Query error: {}", e);
241 map_sqlx_error(e)
242 })?;
243
244 let mut json_rows: Vec<serde_json::Value> = rows
250 .into_iter()
251 .map(|row| postrust_core::row_json::row_to_json(&row))
252 .collect();
253
254 if let Some(parent_qi) =
258 read_target(api_request).filter(|_| embed_expressions.is_empty())
259 {
260 let schema_cache = state.schema_cache().await;
261 embed_relations(
262 &mut tx,
263 &schema_cache,
264 &parent_qi,
265 &api_request.query_params.select,
266 &mut json_rows,
267 api_request.max_rows,
268 )
269 .await?;
270 }
271
272 tx.commit()
275 .await
276 .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
277
278 for column in added_join_columns {
280 for row in json_rows.iter_mut() {
281 if let Some(object) = row.as_object_mut() {
282 object.remove(column);
283 }
284 }
285 }
286
287 let (json_rows, singular) = if state.config.compat_mode {
291 if let ActionPlan::Db(DbActionPlan::Call { call, .. }) = plan {
292 unwrap_rpc_rows(json_rows, call)
293 } else {
294 (json_rows, false)
295 }
296 } else {
297 (json_rows, false)
298 };
299
300 Ok(QueryResult {
301 status: StatusCode::OK,
302 rows: json_rows,
303 singular,
304 ..Default::default()
305 })
306 }
307 ActionPlan::Info(info_plan) => {
308 use postrust_core::plan::InfoPlan;
309
310 let response_data = match info_plan {
312 InfoPlan::OpenApiSpec => {
313 serde_json::json!({
315 "name": "postrust",
316 "version": env!("CARGO_PKG_VERSION"),
317 "description": "PostgREST-compatible REST API for PostgreSQL"
318 })
319 }
320 InfoPlan::RelationInfo(qi) => {
321 serde_json::json!({
322 "schema": qi.schema,
323 "name": qi.name,
324 "type": "relation"
325 })
326 }
327 InfoPlan::RoutineInfo(qi) => {
328 serde_json::json!({
329 "schema": qi.schema,
330 "name": qi.name,
331 "type": "routine"
332 })
333 }
334 };
335
336 Ok(QueryResult {
337 status: StatusCode::OK,
338 rows: vec![response_data],
339 ..Default::default()
340 })
341 }
342 }
343}
344
345fn unwrap_rpc_rows(
359 rows: Vec<serde_json::Value>,
360 call: &CallPlan,
361) -> (Vec<serde_json::Value>, bool) {
362 let singular = !call.returns_set;
363
364 if call.returns_composite {
365 return (rows, singular);
366 }
367
368 let fname = call.function.name.as_str();
369 let unwrapped = rows
370 .into_iter()
371 .map(|row| match row {
372 serde_json::Value::Object(ref map) if map.len() == 1 && map.contains_key(fname) => {
373 map.get(fname).cloned().unwrap_or(serde_json::Value::Null)
374 }
375 other => other,
376 })
377 .collect();
378
379 (unwrapped, singular)
380}
381
382fn read_target(
384 api_request: &postrust_core::api_request::ApiRequest,
385) -> Option<postrust_core::api_request::QualifiedIdentifier> {
386 use postrust_core::api_request::{Action, DbAction};
387
388 match &api_request.action {
389 Action::Db(DbAction::RelationRead { qi, .. }) => Some(qi.clone()),
390 _ => None,
391 }
392}
393
394#[allow(clippy::result_large_err)] fn add_embed_join_columns(
401 api_request: &mut postrust_core::api_request::ApiRequest,
402 schema_cache: &postrust_core::SchemaCache,
403) -> Result<Vec<String>, postrust_core::Error> {
404 use postrust_core::api_request::{Field, SelectItem};
405
406 let Some(parent_qi) = read_target(api_request) else {
407 return Ok(Vec::new());
408 };
409
410 let select = &api_request.query_params.select;
411 if select.is_empty() {
412 return Ok(Vec::new());
413 }
414
415 let selected: std::collections::HashSet<String> = select
416 .iter()
417 .filter_map(|item| match item {
418 SelectItem::Field { field, .. } => Some(field.name.clone()),
419 _ => None,
420 })
421 .collect();
422
423 let mut added = Vec::new();
424
425 for item in select.clone() {
426 let SelectItem::Relation { relation, .. } = &item else {
427 continue;
428 };
429
430 let rel = schema_cache
431 .find_relationship(&parent_qi, relation, &parent_qi.schema)
432 .ok_or_else(|| postrust_core::Error::RelationshipNotFound(relation.clone()))?;
433
434 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
435
436 if !selected.contains(&plan.local_column) && !added.contains(&plan.local_column) {
437 api_request.query_params.select.push(SelectItem::Field {
438 field: Field::simple(&plan.local_column),
439 aggregate: None,
440 aggregate_cast: None,
441 cast: None,
442 alias: None,
443 });
444 added.push(plan.local_column.clone());
445 }
446 }
447
448 Ok(added)
449}
450
451#[allow(clippy::result_large_err)] fn build_embed_expressions(
461 schema_cache: &postrust_core::SchemaCache,
462 parent_qi: &postrust_core::api_request::QualifiedIdentifier,
463 parent_alias: &str,
464 select: &[postrust_core::api_request::SelectItem],
465 max_rows: Option<i64>,
466 alias_counter: &mut usize,
467) -> Result<Vec<(String, String)>, postrust_core::Error> {
468 use postrust_core::api_request::SelectItem;
469
470 let mut expressions = Vec::new();
471
472 for item in select {
473 let SelectItem::Relation {
474 relation,
475 alias,
476 select: nested,
477 ..
478 } = item
479 else {
480 continue;
481 };
482
483 let rel = schema_cache
484 .find_relationship(parent_qi, relation, &parent_qi.schema)
485 .ok_or_else(|| postrust_core::Error::RelationshipNotFound(relation.clone()))?;
486 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
487
488 *alias_counter += 1;
489 let child_alias = format!("e{}", alias_counter);
490 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
491 &plan.foreign_schema,
492 &plan.foreign_table,
493 );
494
495 let nested_expressions = build_embed_expressions(
497 schema_cache,
498 &child_qi,
499 &child_alias,
500 nested,
501 max_rows,
502 alias_counter,
503 )?;
504
505 let mut parts: Vec<String> = Vec::new();
508 let mut project_everything = nested.is_empty();
509 for nested_item in nested {
510 match nested_item {
511 SelectItem::Field { field, alias, .. } => {
512 let column = postrust_sql::escape_ident(&field.name);
513 match alias {
514 Some(alias) => parts.push(format!(
515 "{} AS {}",
516 column,
517 postrust_sql::escape_ident(alias)
518 )),
519 None => parts.push(column),
520 }
521 }
522 SelectItem::Relation { .. } => {}
524 SelectItem::SpreadRelation { .. } => project_everything = true,
525 }
526 }
527 if project_everything {
528 parts.clear();
529 parts.push(format!("{}.*", postrust_sql::escape_ident(&child_alias)));
530 }
531 for (field_name, expression) in nested_expressions {
532 parts.push(format!(
533 "{} AS {}",
534 expression,
535 postrust_sql::escape_ident(&field_name)
536 ));
537 }
538
539 let inner_select = parts.join(", ");
540 let expression =
541 plan.embed_expression(parent_alias, &child_alias, &inner_select, max_rows)?;
542
543 expressions.push((
544 alias.clone().unwrap_or_else(|| relation.clone()),
545 expression,
546 ));
547 }
548
549 Ok(expressions)
550}
551
552type EmbedFuture<'f> = std::pin::Pin<
553 Box<dyn std::future::Future<Output = Result<(), postrust_core::Error>> + Send + 'f>,
554>;
555
556fn embed_relations<'f>(
562 conn: &'f mut sqlx::PgConnection,
563 schema_cache: &'f postrust_core::SchemaCache,
564 parent_qi: &'f postrust_core::api_request::QualifiedIdentifier,
565 select: &'f [postrust_core::api_request::SelectItem],
566 rows: &'f mut [serde_json::Value],
567 max_rows: Option<i64>,
568) -> EmbedFuture<'f> {
569 use postrust_core::api_request::SelectItem;
570
571 Box::pin(async move {
572 if rows.is_empty() {
573 return Ok(());
574 }
575
576 for item in select {
577 let SelectItem::Relation {
578 relation,
579 alias,
580 select: nested,
581 ..
582 } = item
583 else {
584 continue;
585 };
586
587 let rel = schema_cache
588 .find_relationship(parent_qi, relation, &parent_qi.schema)
589 .ok_or_else(|| postrust_core::Error::RelationshipNotFound(relation.clone()))?;
590
591 let plan = postrust_core::embed::EmbedPlan::resolve(rel, schema_cache)?;
592 let keys = postrust_core::embed::parent_keys(rows, &plan.local_column);
593
594 let child_qi = postrust_core::api_request::QualifiedIdentifier::new(
606 &plan.foreign_schema,
607 &plan.foreign_table,
608 );
609
610 let mut child_columns: Vec<String> = Vec::new();
611 let mut project_everything = nested.is_empty();
612 for nested_item in nested {
613 match nested_item {
614 SelectItem::Field { field, .. } => child_columns.push(field.name.clone()),
615 SelectItem::Relation { relation, .. } => {
616 match schema_cache.find_relationship(&child_qi, relation, &child_qi.schema)
617 {
618 Some(nested_rel) => {
619 let nested_plan = postrust_core::embed::EmbedPlan::resolve(
620 nested_rel,
621 schema_cache,
622 )?;
623 child_columns.push(nested_plan.local_column);
624 }
625 None => project_everything = true,
629 }
630 }
631 SelectItem::SpreadRelation { .. } => project_everything = true,
632 }
633 }
634 if project_everything {
635 child_columns.clear();
636 }
637
638 let mut grouped = if keys.is_empty() {
639 std::collections::HashMap::new()
640 } else {
641 let sql = plan.children_grouped_sql(max_rows, &child_columns)?;
642
643 let fetched = sqlx::query(&sql)
644 .bind(&keys)
645 .fetch_all(&mut *conn)
646 .await
647 .map_err(map_sqlx_error)?;
648
649 use sqlx::Row;
654 let pairs: Vec<(serde_json::Value, serde_json::Value)> = fetched
655 .into_iter()
656 .filter_map(|row| {
657 Some((
658 row.try_get::<serde_json::Value, _>(0).ok()?,
659 row.try_get::<serde_json::Value, _>(1).ok()?,
660 ))
661 })
662 .collect();
663
664 postrust_core::embed::group_from_aggregated(pairs)
665 };
666
667 let has_deeper_embed = nested.iter().any(|item| {
675 matches!(
676 item,
677 SelectItem::Relation { .. } | SelectItem::SpreadRelation { .. }
678 )
679 });
680
681 if has_deeper_embed {
682 let mut order: Vec<(String, usize)> = Vec::with_capacity(grouped.len());
683 let mut flat: Vec<serde_json::Value> = Vec::new();
684 for (key, children) in grouped.drain() {
685 order.push((key, children.len()));
686 flat.extend(children);
687 }
688
689 embed_relations(
690 &mut *conn,
691 schema_cache,
692 &child_qi,
693 nested,
694 &mut flat,
695 max_rows,
696 )
697 .await?;
698
699 let mut rest = flat.into_iter();
700 for (key, count) in order {
701 grouped.insert(key, rest.by_ref().take(count).collect());
702 }
703 }
704
705 let requested: Option<std::collections::HashSet<String>> = if nested.is_empty() {
712 None
713 } else {
714 Some(
715 nested
716 .iter()
717 .filter_map(|nested_item| match nested_item {
718 SelectItem::Field { field, alias, .. } => {
719 Some(alias.clone().unwrap_or_else(|| field.name.clone()))
720 }
721 SelectItem::Relation {
722 relation, alias, ..
723 } => Some(alias.clone().unwrap_or_else(|| relation.clone())),
724 SelectItem::SpreadRelation { .. } => None,
725 })
726 .collect(),
727 )
728 };
729
730 if let Some(requested) = requested {
731 for group in grouped.values_mut() {
732 for child in group.iter_mut() {
733 if let Some(object) = child.as_object_mut() {
734 object.retain(|key, _| requested.contains(key));
735 }
736 }
737 }
738 }
739 let field_name = alias.clone().unwrap_or_else(|| relation.clone());
740 for row in rows.iter_mut() {
741 postrust_core::embed::attach_to_parent(row, &field_name, &plan, &grouped);
742 }
743 }
744
745 Ok(())
746 })
747}
748
749fn bind_params<'q>(
751 mut query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
752 params: &'q [postrust_sql::SqlParam],
753) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
754 use postrust_sql::SqlParam;
755
756 for param in params {
757 query = match param {
758 SqlParam::Null => query.bind(None::<String>),
759 SqlParam::Bool(b) => query.bind(b),
760 SqlParam::Int(n) => query.bind(n),
761 SqlParam::Float(f) => query.bind(f),
762 SqlParam::Text(s) => query.bind(s),
763 SqlParam::Bytes(b) => query.bind(b),
764 SqlParam::Json(j) => query.bind(j),
765 SqlParam::Uuid(u) => query.bind(u),
766 SqlParam::Timestamp(t) => query.bind(t),
767 SqlParam::Array(arr) => {
768 let strings: Vec<String> = arr
770 .iter()
771 .map(|p| match p {
772 SqlParam::Text(s) => s.clone(),
773 SqlParam::Int(n) => n.to_string(),
774 SqlParam::Bool(b) => b.to_string(),
775 other => format!("{:?}", other),
776 })
777 .collect();
778 query.bind(strings)
779 }
780 };
781 }
782
783 query
784}
785
786fn map_sqlx_error(e: sqlx::Error) -> postrust_core::Error {
788 match e {
789 sqlx::Error::Database(db_err) => {
790 let (details, hint) = db_err
792 .try_downcast_ref::<sqlx::postgres::PgDatabaseError>()
793 .map(|pg_err| {
794 (
795 pg_err.detail().map(String::from),
796 pg_err.hint().map(String::from),
797 )
798 })
799 .unwrap_or((None, None));
800
801 postrust_core::Error::Database(postrust_core::error::DatabaseError {
802 code: db_err.code().map(|c| c.to_string()).unwrap_or_default(),
803 message: db_err.message().to_string(),
804 details,
805 hint,
806 constraint: db_err.constraint().map(|s| s.to_string()),
807 table: db_err.table().map(|s| s.to_string()),
808 column: None,
809 })
810 }
811 other => postrust_core::Error::Internal(other.to_string()),
812 }
813}
814
815fn build_response(response: PgrstResponse) -> Response {
817 let mut builder = Response::builder().status(response.status);
818
819 for (key, value) in &response.headers {
820 builder = builder.header(key, value);
821 }
822
823 builder
824 .body(Body::from(response.body))
825 .unwrap_or_else(|_| Response::new(Body::empty()))
826}
827
828fn error_response(error: postrust_core::Error) -> Response {
833 let status = error.status_code();
834
835 let debug_mode = std::env::var("PGRST_DEBUG")
837 .map(|v| v == "true" || v == "1")
838 .unwrap_or(false);
839
840 let body = if debug_mode {
841 serde_json::to_vec(&error.to_json()).unwrap_or_default()
843 } else {
844 let sanitized = serde_json::json!({
846 "code": error.code(),
847 "message": sanitize_error_message(&error),
848 "details": null,
849 "hint": null
850 });
851 serde_json::to_vec(&sanitized).unwrap_or_default()
852 };
853
854 Response::builder()
855 .status(status)
856 .header("content-type", "application/json")
857 .body(Body::from(body))
858 .unwrap_or_else(|_| Response::new(Body::empty()))
859}
860
861fn sanitize_error_message(error: &postrust_core::Error) -> &'static str {
863 use postrust_core::Error;
864 match error {
865 Error::TableNotFound(_) | Error::NotFound(_) => "Resource not found",
866 Error::FunctionNotFound(_) => "Function not found",
867 Error::ColumnNotFound(_) | Error::UnknownColumn(_) => "Column not found",
868 Error::RelationshipNotFound(_) => "Relationship not found",
869 Error::InvalidPath(_) => "Invalid request path",
870 Error::InvalidBody(_) => "Invalid request body",
871 Error::InvalidJwt(_) | Error::JwtExpired | Error::MissingAuth => "Unauthorized",
872 Error::InsufficientPermissions(_) => "Forbidden",
873 Error::UnacceptableSchema(_) => "Invalid schema",
874 Error::InvalidHeader(_) | Error::InvalidQueryParam(_) => "Invalid request",
875 Error::Database(_) => "Database error",
876 Error::ConnectionPool(_) => "Service temporarily unavailable",
877 Error::Internal(_) => "Internal server error",
878 _ => "An error occurred",
879 }
880}
881
882#[cfg(test)]
883mod tests {
884 use super::*;
885 use postrust_core::plan::CallParams;
886 use postrust_core::QualifiedIdentifier;
887 use serde_json::json;
888
889 fn call_plan(name: &str, returns_set: bool) -> CallPlan {
890 CallPlan {
891 function: QualifiedIdentifier::new("public", name),
892 params: CallParams::None,
893 returns_scalar: !returns_set,
894 returns_set,
895 returns_composite: false,
896 volatility: "Volatile".into(),
897 }
898 }
899
900 fn composite_call_plan(name: &str, returns_set: bool) -> CallPlan {
901 CallPlan {
902 returns_composite: true,
903 returns_scalar: false,
904 ..call_plan(name, returns_set)
905 }
906 }
907
908 #[test]
909 fn unwraps_json_return_to_bare_object() {
910 let rows = vec![json!({"sync": {"ok": true, "count": 3}})];
913 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("sync", false));
914 assert!(singular, "non-set-returning function should be singular");
915 assert_eq!(rows, vec![json!({"ok": true, "count": 3})]);
916 }
917
918 #[test]
919 fn unwraps_scalar_return() {
920 let rows = vec![json!({"add": 42})];
921 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("add", false));
922 assert!(singular);
923 assert_eq!(rows, vec![json!(42)]);
924 }
925
926 #[test]
927 fn unwraps_setof_scalar_to_array() {
928 let rows = vec![json!({"gen": 1}), json!({"gen": 2})];
929 let (rows, singular) = unwrap_rpc_rows(rows, &call_plan("gen", true));
930 assert!(!singular, "set-returning function should not be singular");
931 assert_eq!(rows, vec![json!(1), json!(2)]);
932 }
933
934 #[test]
935 fn leaves_multi_column_rows_untouched() {
936 let rows = vec![json!({"id": 1, "name": "a"}), json!({"id": 2, "name": "b"})];
939 let (out, singular) =
940 unwrap_rpc_rows(rows.clone(), &composite_call_plan("list_users", true));
941 assert!(!singular);
942 assert_eq!(out, rows);
943 }
944
945 #[test]
946 fn leaves_table_column_named_like_function_untouched() {
947 let rows = vec![json!({"foo": 1}), json!({"foo": 2})];
952 let (out, singular) = unwrap_rpc_rows(rows.clone(), &composite_call_plan("foo", true));
953 assert!(!singular);
954 assert_eq!(out, rows);
955 }
956
957 #[test]
958 fn single_composite_return_is_singular_but_not_unwrapped() {
959 let rows = vec![json!({"id": 1, "name": "a"})];
962 let (out, singular) =
963 unwrap_rpc_rows(rows.clone(), &composite_call_plan("get_user", false));
964 assert!(singular);
965 assert_eq!(out, rows);
966 }
967
968 #[test]
969 fn leaves_single_key_row_untouched_when_key_is_not_function_name() {
970 let rows = vec![json!({"id": 7})];
973 let (out, _) = unwrap_rpc_rows(rows.clone(), &call_plan("get_thing", false));
974 assert_eq!(out, rows);
975 }
976}