Skip to main content

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/// Main request handler.
19pub 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
31/// Process a request and return a response.
32async fn process_request(
33    state: Arc<AppState>,
34    request: Request,
35) -> Result<Response, postrust_core::Error> {
36    // Extract auth header
37    let auth_header = request
38        .headers()
39        .get("authorization")
40        .and_then(|v| v.to_str().ok());
41
42    // Authenticate
43    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    // Parse request
49    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    // Build HTTP request for parsing
55    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    // Parse API request
68    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    // Parse payload
76    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    // Get schema cache
85    let schema_cache = state.schema_cache().await;
86
87    // Embedding joins on a column of the parent row, so that column has to be
88    // selected even when the client did not ask for it. Any column added this
89    // way is removed from the response once the embed is attached.
90    let added_join_columns = add_embed_join_columns(&mut api_request, &schema_cache)?;
91
92    // Create execution plan
93    let plan = create_action_plan(&api_request, &schema_cache)?;
94
95    // Execute plan
96    let result = execute_plan(
97        &state,
98        &api_request,
99        &plan,
100        &auth_result,
101        &added_join_columns,
102    )
103    .await?;
104
105    // Format response
106    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
112/// Execute an action plan.
113async 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            // Build SQL
123            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            // Embed related resources in the same query.
135            //
136            // Each relation becomes a correlated subselect in the SELECT list,
137            // so PostgreSQL builds the nested JSON while it already has the
138            // parent row -- one round trip for the whole tree instead of one per
139            // relation per level, and no grouping or attaching in this process.
140            //
141            // Parent columns stay ordinary typed columns, so they are converted
142            // to JSON by the same code as a request without embeds and a NUMERIC
143            // or timestamp renders identically either way.
144            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            // Everything for this request runs in one transaction.
182            //
183            // `SET LOCAL ROLE` and `set_config(..., true)` are scoped to a
184            // transaction. Sent on a bare pooled connection they apply to their
185            // own implicit single-statement transaction and are gone before the
186            // next statement runs, so the query executed as the pool's login
187            // role with no JWT claims set -- row-level security and role grants
188            // were not being applied at all. PostgreSQL says so in its log:
189            // "SET LOCAL can only be used in transaction blocks".
190            //
191            // Embedding runs on this same transaction, which also means a
192            // parent row and its children come from one snapshot rather than
193            // from two separate reads.
194            let mut tx = state
195                .pool
196                .begin()
197                .await
198                .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
199
200            // Set role
201            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            // Set claims as GUC
220            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(); // Ignore errors for individual claims
233            }
234
235            // Execute main query with bound parameters
236            let rows = bind_params(sqlx::query(&sql), &params)
237                .fetch_all(&mut *tx)
238                .await
239                .map_err(|e| {
240                    error!("Query error: {}", e);
241                    map_sqlx_error(e)
242                })?;
243
244            // Convert rows to JSON.
245            //
246            // `into_iter` matters for large result sets: each row's buffers are
247            // freed as soon as it has been converted, rather than the whole
248            // `Vec<PgRow>` staying alive alongside the whole `Vec<Value>`.
249            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            // Embeds already came back with the parent query when the SELECT
255            // list carried relations. This path remains for anything the
256            // single-query form did not handle.
257            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            // Reads take no locks worth holding and write plans commit their
273            // work, so the transaction is committed either way.
274            tx.commit()
275                .await
276                .map_err(|e| postrust_core::Error::ConnectionPool(e.to_string()))?;
277
278            // Drop columns that were only selected to join the embeds.
279            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            // In PostgREST-compatibility mode, reshape RPC responses to match
288            // PostgREST: un-nest the function-name-keyed column and return a
289            // bare value for non-set-returning functions.
290            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            // Return appropriate metadata based on the info type
311            let response_data = match info_plan {
312                InfoPlan::OpenApiSpec => {
313                    // Return basic server info for root endpoint
314                    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
345/// Reshape RPC rows for PostgREST-compatibility mode.
346///
347/// `SELECT * FROM func(...)` names its single output column after the function
348/// (for scalar/`json` returns), which serializes to rows like
349/// `[{"func": <value>}]`. PostgREST instead returns the bare value. This
350/// un-nests that wrapper column and reports whether the result should be
351/// rendered as a single (un-arrayed) value — i.e. when the function is not
352/// set-returning.
353///
354/// The decision is driven by the plan's return-type metadata: composite and
355/// `record` returns (`RETURNS TABLE`, row types) have real output columns and
356/// are never un-nested, even when a single column happens to share the
357/// function's name (e.g. `CREATE FUNCTION foo() RETURNS TABLE(foo int)`).
358fn 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
382/// The table a read targets, if the request is a read.
383fn 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/// Ensure every embedded relation's join column is selected on the parent.
395///
396/// Returns the columns that were added purely to make the join possible, so
397/// they can be removed from the response afterwards. An empty select list means
398/// "all columns", in which case nothing needs adding.
399#[allow(clippy::result_large_err)] // consistent with the crate's error type
400fn 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/// Build the SELECT-list expressions that embed relations in one query.
452///
453/// Returns one `(field_name, expression)` per requested relation. Each
454/// expression is a correlated subselect yielding JSON, so the whole tree comes
455/// back from the parent query rather than from a query per relation per level.
456///
457/// `alias_counter` hands out a distinct alias per level, so a self-referential
458/// relationship stays unambiguous.
459#[allow(clippy::result_large_err)] // consistent with the crate's error type
460fn 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        // Deeper relations first: they become part of this level's SELECT list.
496        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        // The child's own columns. Empty means every column, which is what a
506        // relation with no explicit selection asks for.
507        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                // Handled as an expression, not a column.
523                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
556/// Attach the relations requested in `select` onto `rows`.
557///
558/// One query per relation per level: the parents' join keys are collected and
559/// passed as a single array, so embedding across a page of parents does not
560/// become a query per row. Recurses for nested embeds.
561fn 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            // Which of the child's columns the query needs to return.
595            //
596            // A column that was not asked for still costs a heap read, JSON
597            // serialisation in PostgreSQL, socket bytes and a parse before
598            // being discarded, so the projection is worked out before the query
599            // rather than filtered afterwards.
600            //
601            // A nested embed joins on a column of the child row, so that column
602            // has to survive the projection even when the client did not ask
603            // for it. A spread relation falls back to every column: its shape
604            // is decided further down and is not a plain projection.
605            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                            // Leave it to the recursive call to report the
626                            // unknown relation, rather than failing here with
627                            // less context.
628                            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                // The query returns the join key and a JSON array of that
650                // key's children, so the values are read straight out of the
651                // columns -- the typed row converter would wrap them under the
652                // expression name.
653                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            // Deeper embeds, if any were asked for.
668            //
669            // A nested embed issues one query for every child row at this
670            // level, so the rows have to be contiguous for it. They arrive
671            // grouped, so they are flattened for the recursive call and put
672            // back afterwards. When nothing deeper was requested this is all
673            // skipped, which is the common case.
674            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            // Return only the requested columns of the related resource. An
706            // empty nested select means every column.
707            //
708            // The projection in the query covers the columns; this covers what
709            // is left over, which is the join column when the client did not
710            // ask for it.
711            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
749/// Bind SqlParam values to a sqlx query.
750fn 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                // Convert array to Vec<String> for text arrays
769                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
786/// Map sqlx error to our error type.
787fn map_sqlx_error(e: sqlx::Error) -> postrust_core::Error {
788    match e {
789        sqlx::Error::Database(db_err) => {
790            // Try to downcast to Postgres-specific error for additional details
791            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
815/// Build an HTTP response from our response type.
816fn 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
828/// Build an error response.
829///
830/// In production mode (PGRST_DEBUG=false or unset), sensitive error details
831/// are hidden to prevent information leakage.
832fn error_response(error: postrust_core::Error) -> Response {
833    let status = error.status_code();
834
835    // Check if debug mode is enabled
836    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        // Full error details in debug mode
842        serde_json::to_vec(&error.to_json()).unwrap_or_default()
843    } else {
844        // Sanitized error in production
845        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
861/// Sanitize error messages for production.
862fn 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        // `SELECT * FROM sync(...)` on a json-returning function yields a single
911        // column named after the function.
912        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        // `RETURNS TABLE(...)` / composite set: rows already have real columns
937        // and must not be un-nested.
938        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        // `CREATE FUNCTION foo() RETURNS TABLE(foo int)`: the single output
948        // column legitimately shares the function's name. The composite
949        // return-type metadata must prevent it from being mistaken for the
950        // function-name wrapper.
951        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        // A non-set function returning a row type expands to its columns;
960        // nothing to un-nest, but the result still renders as a bare object.
961        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        // A single real column that happens to be the only column should not be
971        // mistaken for the function-name wrapper.
972        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}