Skip to main content

postrust_graphql/
handler.rs

1//! Axum handler for the /graphql endpoint.
2//!
3//! Provides GraphQL request handling using async-graphql with dynamic schema
4//! generation from the PostgreSQL schema cache.
5
6use crate::context::GraphQLContext;
7use crate::error::GraphQLError;
8use crate::schema::object::TableObjectType;
9use crate::schema::relationship::RelationshipField;
10use crate::schema::{build_schema, GeneratedSchema, MutationType, SchemaConfig};
11use crate::subscription::{
12    generate_subscription_fields, NotifyBroker, SubscriptionField as SubField, TableChangePayload,
13};
14use async_graphql::dynamic::*;
15use async_graphql::Value;
16use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
17use axum::extract::State;
18use axum::response::IntoResponse;
19use futures::stream::StreamExt;
20use postrust_core::schema_cache::SchemaCache;
21use sqlx::PgPool;
22use std::collections::HashMap;
23use std::sync::Arc;
24use tokio::sync::RwLock;
25use tracing::{debug, info, trace};
26
27/// GraphQL execution state shared across requests.
28pub struct GraphQLState {
29    /// Database connection pool
30    pub pool: PgPool,
31    /// Schema cache
32    pub schema_cache: Arc<SchemaCache>,
33    /// Generated GraphQL schema
34    pub generated_schema: GeneratedSchema,
35    /// async-graphql Schema (built dynamically)
36    pub schema: Schema,
37    /// Schema configuration
38    pub config: SchemaConfig,
39    /// Subscription fields
40    pub subscription_fields: Vec<SubField>,
41    /// Notification broker for subscriptions
42    pub broker: Arc<RwLock<Option<NotifyBroker>>>,
43}
44
45impl GraphQLState {
46    /// Create new GraphQL state from schema cache.
47    pub fn new(
48        pool: PgPool,
49        schema_cache: Arc<SchemaCache>,
50        config: SchemaConfig,
51    ) -> Result<Self, GraphQLError> {
52        let generated_schema = build_schema(&schema_cache, &config);
53        let subscription_fields = if config.enable_subscriptions {
54            generate_subscription_fields(&schema_cache, &generated_schema)
55        } else {
56            Vec::new()
57        };
58        let schema = build_dynamic_schema(
59            &generated_schema,
60            &schema_cache,
61            if config.enable_subscriptions {
62                Some(subscription_fields.as_slice())
63            } else {
64                None
65            },
66            config.max_rows,
67        )?;
68
69        Ok(Self {
70            pool: pool.clone(),
71            schema_cache,
72            generated_schema,
73            schema,
74            config,
75            subscription_fields,
76            broker: Arc::new(RwLock::new(None)),
77        })
78    }
79
80    /// Rebuild the schema (e.g., after schema cache refresh).
81    pub fn rebuild(&mut self) -> Result<(), GraphQLError> {
82        self.generated_schema = build_schema(&self.schema_cache, &self.config);
83        self.subscription_fields = if self.config.enable_subscriptions {
84            generate_subscription_fields(&self.schema_cache, &self.generated_schema)
85        } else {
86            Vec::new()
87        };
88        self.schema = build_dynamic_schema(
89            &self.generated_schema,
90            &self.schema_cache,
91            if self.config.enable_subscriptions {
92                Some(self.subscription_fields.as_slice())
93            } else {
94                None
95            },
96            self.config.max_rows,
97        )?;
98        Ok(())
99    }
100
101    /// Initialize the subscription broker.
102    ///
103    /// This should be called after creating the state to enable subscriptions.
104    pub async fn init_subscriptions(&self) -> Result<(), crate::subscription::BrokerError> {
105        if !self.config.enable_subscriptions {
106            return Ok(());
107        }
108
109        let broker = NotifyBroker::new(self.pool.clone());
110
111        // Collect all channels to listen on
112        let channels: Vec<String> = self
113            .subscription_fields
114            .iter()
115            .map(|f| f.channel_name())
116            .collect();
117
118        if !channels.is_empty() {
119            broker.start(channels).await?;
120            info!(
121                "Subscription broker started with {} channels",
122                self.subscription_fields.len()
123            );
124        }
125
126        // Store the broker
127        let mut broker_guard = self.broker.write().await;
128        *broker_guard = Some(broker);
129
130        Ok(())
131    }
132
133    /// Stop the subscription broker.
134    pub async fn stop_subscriptions(&self) {
135        let broker_guard = self.broker.read().await;
136        if let Some(broker) = broker_guard.as_ref() {
137            broker.stop().await;
138        }
139    }
140
141    /// Get the notification broker.
142    pub async fn get_broker(&self) -> Option<Arc<RwLock<Option<NotifyBroker>>>> {
143        Some(Arc::clone(&self.broker))
144    }
145}
146
147/// Handle a GraphQL request.
148pub async fn graphql_handler(
149    State(state): State<Arc<GraphQLState>>,
150    ctx: GraphQLContext,
151    req: GraphQLRequest,
152) -> GraphQLResponse {
153    let request = req
154        .into_inner()
155        .data(ctx)
156        .data(state.pool.clone())
157        .data(Arc::clone(&state.broker));
158    state.schema.execute(request).await.into()
159}
160
161/// Handle GraphQL WebSocket subscription upgrade.
162///
163/// This should be called with a WebSocket upgrade request to enable
164/// GraphQL subscriptions over WebSocket.
165pub async fn graphql_ws_handler(
166    State(state): State<Arc<GraphQLState>>,
167    protocol: async_graphql_axum::GraphQLProtocol,
168    ws: axum::extract::WebSocketUpgrade,
169) -> impl IntoResponse {
170    let schema = state.schema.clone();
171    let pool = state.pool.clone();
172    let broker = Arc::clone(&state.broker);
173
174    ws.protocols(["graphql-transport-ws", "graphql-ws"])
175        .on_upgrade(move |socket| async move {
176            let mut data = async_graphql::Data::default();
177            data.insert(pool);
178            data.insert(broker);
179
180            async_graphql_axum::GraphQLWebSocket::new(socket, schema, protocol)
181                .with_data(data)
182                .serve()
183                .await
184        })
185}
186
187/// Handle GraphQL playground request.
188pub async fn graphql_playground() -> impl axum::response::IntoResponse {
189    axum::response::Html(async_graphql::http::playground_source(
190        async_graphql::http::GraphQLPlaygroundConfig::new("/graphql")
191            .subscription_endpoint("/graphql/ws"),
192    ))
193}
194
195/// Build the dynamic async-graphql schema from our generated schema.
196fn build_dynamic_schema(
197    generated: &GeneratedSchema,
198    _schema_cache: &SchemaCache,
199    subscription_fields: Option<&[SubField]>,
200    max_rows: Option<i64>,
201) -> Result<Schema, GraphQLError> {
202    // Create object types for each table
203    let mut object_types: HashMap<String, Object> = HashMap::new();
204
205    for (type_name, obj) in &generated.object_types {
206        let relationships = generated
207            .relationship_fields
208            .get(type_name)
209            .map(|r| r.as_slice())
210            .unwrap_or(&[]);
211        let table_obj = create_object_type(obj, relationships);
212        object_types.insert(type_name.clone(), table_obj);
213    }
214
215    // Create query type. Resolvers need the relationship map to embed related
216    // rows, so it is shared into each closure.
217    let relationships = Arc::new(generated.relationship_fields.clone());
218    let query = create_query_type(generated, max_rows, Arc::clone(&relationships));
219
220    // Create mutation type
221    let mutation = if !generated.mutation_fields.is_empty() {
222        Some(create_mutation_type(generated))
223    } else {
224        None
225    };
226
227    // Create subscription type if enabled
228    let subscription = subscription_fields.map(create_subscription_type);
229
230    // Build schema
231    let mut builder = Schema::build(
232        "Query",
233        mutation.as_ref().map(|_| "Mutation"),
234        subscription.as_ref().map(|_| "Subscription"),
235    );
236
237    // Register all object types
238    for (_, obj) in object_types {
239        builder = builder.register(obj);
240    }
241
242    // Register query type
243    builder = builder.register(query);
244
245    // Register mutation type if present
246    if let Some(mutation) = mutation {
247        builder = builder.register(mutation);
248    }
249
250    // Register subscription type if present
251    if let Some(subscription) = subscription {
252        builder = builder.register(subscription);
253    }
254
255    // Register scalar types
256    builder = builder.register(create_bigint_scalar());
257    builder = builder.register(create_bigdecimal_scalar());
258    builder = builder.register(create_json_scalar());
259    builder = builder.register(create_uuid_scalar());
260    builder = builder.register(create_date_scalar());
261    builder = builder.register(create_datetime_scalar());
262    builder = builder.register(create_time_scalar());
263
264    // Register input types
265    builder = register_filter_input_types(builder);
266
267    builder
268        .finish()
269        .map_err(|e| GraphQLError::SchemaError(e.to_string()))
270}
271
272/// Create an object type from a TableObjectType.
273fn create_object_type(obj: &TableObjectType, relationships: &[RelationshipField]) -> Object {
274    let mut object = Object::new(&obj.name);
275
276    if let Some(desc) = obj.description() {
277        object = object.description(desc);
278    }
279
280    for field in &obj.fields {
281        let field_name = field.name.clone();
282        let field_type = graphql_type_ref(&field.type_string());
283
284        // Create field with resolver that extracts from parent async_graphql::Value
285        // The query resolver stores rows as FieldValue::value(Value::Object)
286        // so we use as_value() to get the Value and extract fields from the Object
287        let gql_field = Field::new(&field.name, field_type, move |ctx| {
288            let field_name = field_name.clone();
289            FieldFuture::new(async move {
290                // Get the parent value as async_graphql::Value using as_value()
291                if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
292                    // Convert field name to async_graphql::Name for lookup
293                    let key = async_graphql::Name::new(&field_name);
294                    if let Some(val) = map.get(&key) {
295                        return Ok(Some(FieldValue::value(val.clone())));
296                    }
297                }
298
299                // Field not found or parent not a Value::Object
300                Ok(None)
301            })
302        });
303
304        let gql_field = if let Some(desc) = &field.description {
305            gql_field.description(desc)
306        } else {
307            gql_field
308        };
309
310        object = object.field(gql_field);
311    }
312
313    // Relationship fields. The query resolver embeds related rows into the
314    // parent JSON before returning it, so these read from the parent value the
315    // same way column fields do.
316    for rel in relationships {
317        let field_name = rel.name.clone();
318        let field_type = if rel.is_list {
319            TypeRef::named_nn_list_nn(&rel.target_type)
320        } else {
321            TypeRef::named(&rel.target_type)
322        };
323
324        let gql_field = Field::new(&rel.name, field_type, move |ctx| {
325            let field_name = field_name.clone();
326            FieldFuture::new(async move {
327                if let Some(Value::Object(map)) = ctx.parent_value.as_value() {
328                    let key = async_graphql::Name::new(&field_name);
329                    if let Some(val) = map.get(&key) {
330                        return Ok(Some(FieldValue::value(val.clone())));
331                    }
332                }
333                Ok(None)
334            })
335        });
336
337        let gql_field = if let Some(desc) = &rel.description {
338            gql_field.description(desc)
339        } else {
340            gql_field
341        };
342
343        object = object.field(gql_field);
344    }
345
346    object
347}
348
349/// Create the Query type with all table query fields.
350fn create_query_type(
351    generated: &GeneratedSchema,
352    max_rows: Option<i64>,
353    relationships: Arc<HashMap<String, Vec<RelationshipField>>>,
354) -> Object {
355    let mut query = Object::new("Query");
356
357    for field in &generated.query_fields {
358        let table_name = field.table_name.clone();
359        let schema_name = field.schema_name.clone();
360        let type_name = field.type_name.clone();
361        let is_by_pk = field.is_by_pk;
362        let pk_columns = field.pk_columns.clone();
363        let return_type = graphql_type_ref(&field.return_type);
364
365        let spec = Arc::new(QueryFieldSpec {
366            schema_name,
367            table_name,
368            type_name,
369            is_by_pk,
370            pk_columns: pk_columns.clone(),
371            max_rows,
372            relationships: Arc::clone(&relationships),
373        });
374
375        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
376            let spec = Arc::clone(&spec);
377            FieldFuture::new(async move { resolve_query(&ctx, &spec).await })
378        });
379
380        // Add standard query arguments
381        if !is_by_pk {
382            gql_field = gql_field
383                .argument(InputValue::new("filter", TypeRef::named("JSON")))
384                .argument(InputValue::new("orderBy", TypeRef::named_list("String")))
385                .argument(InputValue::new("limit", TypeRef::named("Int")))
386                .argument(InputValue::new("offset", TypeRef::named("Int")));
387        } else {
388            // One required argument per primary key column, named and typed
389            // after the column itself rather than assuming an integer `id`.
390            for (col_name, pg_type) in &pk_columns {
391                gql_field = gql_field.argument(InputValue::new(
392                    col_name,
393                    TypeRef::named_nn(pk_argument_type(pg_type)),
394                ));
395            }
396        }
397
398        if let Some(desc) = &field.description {
399            gql_field = gql_field.description(desc);
400        }
401
402        query = query.field(gql_field);
403    }
404
405    // Add introspection queries
406    query = query.field(
407        Field::new("_schema", TypeRef::named("String"), |_| {
408            FieldFuture::new(async move {
409                Ok(Some(Value::String("Postrust GraphQL Schema".to_string())))
410            })
411        })
412        .description("Schema introspection"),
413    );
414
415    query
416}
417
418/// Create the Mutation type with all mutation fields.
419fn create_mutation_type(generated: &GeneratedSchema) -> Object {
420    let mut mutation = Object::new("Mutation");
421
422    for field in &generated.mutation_fields {
423        let table_name = field.table_name.clone();
424        let schema_name = field.schema_name.clone();
425        let mutation_type = field.mutation_type;
426        let pk_columns = field.pk_columns.clone();
427        let return_type = graphql_type_ref(&field.return_type);
428
429        let resolver_pk_columns = pk_columns.clone();
430        let mut gql_field = Field::new(&field.name, return_type, move |ctx| {
431            let table_name = table_name.clone();
432            let schema_name = schema_name.clone();
433            let pk_columns = resolver_pk_columns.clone();
434            FieldFuture::new(async move {
435                resolve_mutation(&ctx, &schema_name, &table_name, mutation_type, &pk_columns).await
436            })
437        });
438
439        // Add mutation-specific arguments.
440        //
441        // A by-PK mutation takes the key columns rather than a `where` object:
442        // it is meant to address exactly one row, and accepting `where` made it
443        // an ordinary bulk mutation that happened to return the first result.
444        match mutation_type {
445            MutationType::Insert | MutationType::InsertOne => {
446                gql_field =
447                    gql_field.argument(InputValue::new("objects", TypeRef::named_nn_list("JSON")));
448            }
449            MutationType::UpdateByPk => {
450                gql_field = gql_field.argument(InputValue::new("set", TypeRef::named_nn("JSON")));
451                for (col_name, pg_type) in &pk_columns {
452                    gql_field = gql_field.argument(InputValue::new(
453                        col_name,
454                        TypeRef::named_nn(pk_argument_type(pg_type)),
455                    ));
456                }
457            }
458            MutationType::Update => {
459                gql_field = gql_field
460                    .argument(InputValue::new("where", TypeRef::named("JSON")))
461                    .argument(InputValue::new("set", TypeRef::named_nn("JSON")));
462            }
463            MutationType::DeleteByPk => {
464                for (col_name, pg_type) in &pk_columns {
465                    gql_field = gql_field.argument(InputValue::new(
466                        col_name,
467                        TypeRef::named_nn(pk_argument_type(pg_type)),
468                    ));
469                }
470            }
471            MutationType::Delete => {
472                gql_field = gql_field.argument(InputValue::new("where", TypeRef::named("JSON")));
473            }
474        }
475
476        if let Some(desc) = &field.description {
477            gql_field = gql_field.description(desc);
478        }
479
480        mutation = mutation.field(gql_field);
481    }
482
483    mutation
484}
485
486/// Create the Subscription type with all subscription fields.
487fn create_subscription_type(fields: &[SubField]) -> Subscription {
488    let mut subscription = Subscription::new("Subscription");
489
490    for field in fields {
491        let channel_name = field.channel_name();
492        let return_type = TypeRef::named(&field.return_type);
493        let field_name = field.name.clone();
494        let description = field.description.clone();
495
496        let gql_field = SubscriptionField::new(&field_name, return_type, move |ctx| {
497            let channel_name = channel_name.clone();
498            SubscriptionFieldFuture::new(async move {
499                let broker_arc = ctx.data::<Arc<RwLock<Option<NotifyBroker>>>>()?;
500                let broker_guard = broker_arc.read().await;
501
502                let broker = broker_guard.as_ref().ok_or_else(|| {
503                    async_graphql::Error::new("Subscription broker not initialized")
504                })?;
505
506                let stream = broker
507                    .subscribe(&channel_name)
508                    .await
509                    .map_err(|e| async_graphql::Error::new(format!("Subscription error: {}", e)))?;
510
511                // Transform notification stream to GraphQL values
512                // Use FieldValue::value() so field resolvers can use as_value()
513                let value_stream = stream.filter_map(|notification| async move {
514                    match TableChangePayload::from_payload(&notification.payload) {
515                        Ok(payload) => payload
516                            .data()
517                            .map(|data| Ok(FieldValue::value(json_to_value(data.clone())))),
518                        Err(e) => {
519                            debug!("Failed to parse notification payload: {}", e);
520                            None
521                        }
522                    }
523                });
524
525                Ok(value_stream)
526            })
527        });
528
529        let gql_field = if let Some(desc) = description {
530            gql_field.description(desc)
531        } else {
532            gql_field
533        };
534
535        subscription = subscription.field(gql_field);
536    }
537
538    subscription
539}
540
541/// Everything a query field's resolver needs about the field it serves.
542struct QueryFieldSpec {
543    schema_name: String,
544    table_name: String,
545    type_name: String,
546    is_by_pk: bool,
547    pk_columns: Vec<(String, String)>,
548    max_rows: Option<i64>,
549    relationships: Arc<HashMap<String, Vec<RelationshipField>>>,
550}
551
552/// Resolve a query field.
553async fn resolve_query<'a>(
554    ctx: &ResolverContext<'a>,
555    spec: &QueryFieldSpec,
556) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
557    let schema_name = spec.schema_name.as_str();
558    let table_name = spec.table_name.as_str();
559    let type_name = spec.type_name.as_str();
560    let is_by_pk = spec.is_by_pk;
561    let pk_columns = spec.pk_columns.as_slice();
562    let max_rows = spec.max_rows;
563    let relationships = spec.relationships.as_ref();
564
565    let pool = ctx.data::<PgPool>()?;
566    let gql_ctx = ctx.data::<GraphQLContext>()?;
567
568    debug!("Resolving query for table: {}", table_name);
569
570    // Extract pagination arguments
571    let requested_limit: Option<i64> = ctx.args.try_get("limit").ok().and_then(|v| v.i64().ok());
572
573    let offset: Option<i64> = ctx.args.try_get("offset").ok().and_then(|v| v.i64().ok());
574
575    // A query that names no limit would otherwise select the whole table, so
576    // the configured ceiling is applied as the limit in that case, and as an
577    // upper bound when the query asks for more than it. A by-PK query resolves
578    // to at most one row.
579    let limit: Option<i64> = if is_by_pk {
580        Some(1)
581    } else {
582        match (requested_limit, max_rows) {
583            (Some(requested), Some(ceiling)) => Some(requested.min(ceiling)),
584            (Some(requested), None) => Some(requested),
585            (None, ceiling) => ceiling,
586        }
587    };
588
589    // Build the WHERE clause.
590    //
591    // A by-PK query filters on the table's key columns; each value is bound as
592    // a parameter and cast to the column's type, since GraphQL scalars and
593    // PostgreSQL types do not line up (a `uuid` key arrives as a String). A
594    // list query filters on the `filter` argument, which takes the same shape
595    // as a mutation's `where`.
596    let mut where_sql = String::new();
597    let mut bound_values: Vec<serde_json::Value> = Vec::new();
598
599    if is_by_pk {
600        if pk_columns.is_empty() {
601            return Err(async_graphql::Error::new(format!(
602                "\"{}\" has no primary key, so it cannot be queried by key",
603                table_name
604            )));
605        }
606
607        let mut conditions = Vec::with_capacity(pk_columns.len());
608        for (idx, (col_name, pg_type)) in pk_columns.iter().enumerate() {
609            let value = ctx.args.try_get(col_name).map_err(|_| {
610                async_graphql::Error::new(format!(
611                    "missing required primary key argument \"{}\"",
612                    col_name
613                ))
614            })?;
615
616            conditions.push(format!(
617                "{} = ${}::{}",
618                postrust_sql::escape_ident(col_name),
619                idx + 1,
620                pg_type
621            ));
622            bound_values.push(accessor_to_json(&value));
623        }
624        where_sql = format!(" WHERE {}", conditions.join(" AND "));
625    } else if let Some(filter) = ctx
626        .args
627        .try_get("filter")
628        .ok()
629        .map(|v| accessor_to_json(&v))
630    {
631        let (filter_sql, filter_values) = build_where_clause(Some(&filter), 1)?;
632        if !filter_sql.is_empty() {
633            where_sql = format!(" {}", filter_sql);
634            bound_values = filter_values;
635        }
636    }
637
638    // Build the ORDER BY clause from the `orderBy` argument. Entries are
639    // `column`, `column.asc` or `column.desc`; the column is validated against
640    // the table so an unknown or crafted name cannot reach the SQL.
641    let order_sql = if is_by_pk {
642        String::new()
643    } else {
644        build_order_by_clause(ctx, &gql_ctx.schema_cache, schema_name, table_name).await?
645    };
646
647    // ORDER BY, LIMIT and OFFSET belong inside the subquery: applying them to
648    // the outer `row_to_json` projection would leave the ordering of the rows
649    // that survive the limit unspecified.
650    let mut inner = format!(
651        "SELECT * FROM {}.{}{}{}",
652        postrust_sql::escape_ident(schema_name),
653        postrust_sql::escape_ident(table_name),
654        where_sql,
655        order_sql
656    );
657
658    if let Some(limit) = limit {
659        inner.push_str(&format!(" LIMIT {}", limit));
660    }
661
662    if let Some(offset) = offset {
663        inner.push_str(&format!(" OFFSET {}", offset));
664    }
665
666    // Embed the requested relationships in this same query, as correlated
667    // subselects in the SELECT list, so the whole selection is one round trip.
668    let embed_expressions = {
669        let guard = gql_ctx
670            .schema_cache
671            .get()
672            .await
673            .map_err(|e| async_graphql::Error::new(e.to_string()))?;
674        match guard.as_ref() {
675            Some(cache) => build_embed_expressions(
676                cache,
677                relationships,
678                type_name,
679                "src",
680                ctx.field(),
681                max_rows,
682                &mut 0,
683            )?,
684            None => Vec::new(),
685        }
686    };
687
688    let inner = if embed_expressions.is_empty() {
689        inner
690    } else {
691        let mut projection = String::from("src.*");
692        for (field_name, expression) in &embed_expressions {
693            projection.push_str(", ");
694            projection.push_str(expression);
695            projection.push_str(" AS ");
696            projection.push_str(&postrust_sql::escape_ident(field_name));
697        }
698        format!("SELECT {} FROM ({}) AS src", projection, inner)
699    };
700
701    let sql = format!("SELECT row_to_json(t) FROM ({}) t", inner);
702
703    // One transaction for the query and any embeds hanging off it, so the role
704    // applies to all of them and the parent and child rows come from a single
705    // snapshot.
706    let mut tx = begin_with_role(pool, gql_ctx.role()).await?;
707
708    // Execute query - returns Vec<serde_json::Value>
709    let mut result = execute_query_on(&mut tx, &sql, &bound_values).await?;
710
711    // Anything the single-query form did not cover.
712    if embed_expressions.is_empty() {
713        let guard = gql_ctx
714            .schema_cache
715            .get()
716            .await
717            .map_err(|e| async_graphql::Error::new(e.to_string()))?;
718        let cache = guard
719            .as_ref()
720            .ok_or_else(|| async_graphql::Error::new("schema cache is not loaded"))?;
721
722        let embed_ctx = EmbedContext {
723            schema_cache: cache,
724            relationships,
725            max_rows,
726        };
727
728        embed_relationships(&mut tx, &embed_ctx, type_name, ctx.field(), &mut result).await?;
729    }
730
731    tx.commit().await?;
732
733    if is_by_pk {
734        // Return single item as Value::Object
735        // json_to_value converts serde_json to async_graphql Value
736        Ok(result
737            .into_iter()
738            .next()
739            .map(|v| FieldValue::value(json_to_value(v))))
740    } else {
741        // Return list with each item as Value::Object
742        let items: Vec<FieldValue> = result
743            .into_iter()
744            .map(|v| FieldValue::value(json_to_value(v)))
745            .collect();
746        Ok(Some(FieldValue::list(items)))
747    }
748}
749
750/// Resolve a mutation field.
751async fn resolve_mutation<'a>(
752    ctx: &ResolverContext<'a>,
753    schema_name: &str,
754    table_name: &str,
755    mutation_type: MutationType,
756    pk_columns: &[(String, String)],
757) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
758    let pool = ctx.data::<PgPool>()?;
759    let gql_ctx = ctx.data::<GraphQLContext>()?;
760
761    debug!(
762        "Resolving mutation for table: {} type: {:?}",
763        table_name, mutation_type
764    );
765
766    let result = match mutation_type {
767        MutationType::Insert | MutationType::InsertOne => {
768            let objects = ctx
769                .args
770                .try_get("objects")
771                .ok()
772                .map(|v| accessor_to_json(&v))
773                .unwrap_or_else(|| serde_json::Value::Array(vec![]));
774
775            execute_insert(
776                pool,
777                schema_name,
778                table_name,
779                gql_ctx.role(),
780                objects,
781                mutation_type,
782            )
783            .await?
784        }
785        MutationType::Update | MutationType::UpdateByPk => {
786            let set_value = ctx
787                .args
788                .try_get("set")
789                .ok()
790                .map(|v| accessor_to_json(&v))
791                .unwrap_or_else(|| serde_json::json!({}));
792
793            let where_clause = if mutation_type == MutationType::UpdateByPk {
794                Some(pk_where_from_args(ctx, table_name, pk_columns)?)
795            } else {
796                ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v))
797            };
798
799            execute_update(
800                pool,
801                schema_name,
802                table_name,
803                gql_ctx.role(),
804                set_value,
805                where_clause,
806                mutation_type,
807            )
808            .await?
809        }
810        MutationType::Delete | MutationType::DeleteByPk => {
811            let where_clause = if mutation_type == MutationType::DeleteByPk {
812                Some(pk_where_from_args(ctx, table_name, pk_columns)?)
813            } else {
814                ctx.args.try_get("where").ok().map(|v| accessor_to_json(&v))
815            };
816
817            execute_delete(
818                pool,
819                schema_name,
820                table_name,
821                gql_ctx.role(),
822                where_clause,
823                mutation_type,
824            )
825            .await?
826        }
827    };
828
829    Ok(result)
830}
831
832/// Begin a transaction with the request's role applied.
833///
834/// The role has to be set inside a transaction. `SET LOCAL` sent on a bare
835/// pooled connection applies to its own implicit single-statement transaction
836/// and is discarded before the next statement runs, so the query would execute
837/// as the pool's login role -- row-level security and role grants bypassed.
838/// PostgreSQL logs "SET LOCAL can only be used in transaction blocks" every
839/// time it happens.
840async fn begin_with_role(
841    pool: &PgPool,
842    role: &str,
843) -> Result<sqlx::Transaction<'static, sqlx::Postgres>, async_graphql::Error> {
844    let mut tx = pool.begin().await?;
845    sqlx::query(&format!(
846        "SET LOCAL ROLE {}",
847        postrust_sql::escape_ident(role)
848    ))
849    .execute(&mut *tx)
850    .await?;
851    Ok(tx)
852}
853
854/// Execute a SQL query and return results as serde_json::Value.
855/// We keep data as serde_json::Value so field resolvers can use try_downcast_ref.
856async fn execute_query_on(
857    conn: &mut sqlx::PgConnection,
858    sql: &str,
859    params: &[serde_json::Value],
860) -> Result<Vec<serde_json::Value>, async_graphql::Error> {
861    use sqlx::Row;
862
863    trace!("Executing SQL: {}", sql);
864
865    // Execute query
866    let mut query = sqlx::query(sql);
867    for param in params {
868        query = bind_json_value(query, param);
869    }
870    let rows = query.fetch_all(&mut *conn).await?;
871
872    // Return raw JSON values - don't convert to async_graphql::Value
873    // This allows field resolvers to use try_downcast_ref::<serde_json::Value>()
874    let results: Vec<serde_json::Value> = rows
875        .into_iter()
876        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
877        .collect();
878
879    Ok(results)
880}
881
882/// Execute an insert mutation.
883async fn execute_insert<'a>(
884    pool: &PgPool,
885    schema_name: &str,
886    table_name: &str,
887    role: &str,
888    objects: serde_json::Value,
889    mutation_type: MutationType,
890) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
891    use sqlx::Row;
892
893    trace!("Insert mutation for {}: {:?}", table_name, objects);
894
895    // Handle both array and single object
896    let objects_array = match objects {
897        serde_json::Value::Array(arr) => arr,
898        serde_json::Value::Object(obj) => vec![serde_json::Value::Object(obj)],
899        _ => {
900            return Err(async_graphql::Error::new(
901                "objects must be an array or object",
902            ))
903        }
904    };
905
906    if objects_array.is_empty() {
907        return Err(async_graphql::Error::new("objects cannot be empty"));
908    }
909
910    let mut conn = begin_with_role(pool, role).await?;
911
912    let mut inserted: Vec<FieldValue> = Vec::new();
913
914    for obj in objects_array {
915        if let serde_json::Value::Object(map) = obj {
916            // Build INSERT query
917            let columns: Vec<&str> = map.keys().map(|k| k.as_str()).collect();
918            let placeholders: Vec<String> =
919                (1..=columns.len()).map(|i| format!("${}", i)).collect();
920
921            let sql = format!(
922                "INSERT INTO {}.{} ({}) VALUES ({}) RETURNING row_to_json({}.{}.*)",
923                postrust_sql::escape_ident(schema_name),
924                postrust_sql::escape_ident(table_name),
925                columns
926                    .iter()
927                    .map(|c| postrust_sql::escape_ident(c))
928                    .collect::<Vec<_>>()
929                    .join(", "),
930                placeholders.join(", "),
931                postrust_sql::escape_ident(schema_name),
932                postrust_sql::escape_ident(table_name)
933            );
934
935            trace!("Executing INSERT SQL: {}", sql);
936
937            // Build query with parameters
938            let mut query = sqlx::query(&sql);
939            for col in &columns {
940                if let Some(val) = map.get(*col) {
941                    query = bind_json_value(query, val);
942                }
943            }
944
945            let row = query.fetch_one(&mut *conn).await?;
946
947            if let Ok(json_val) = row.try_get::<serde_json::Value, _>(0) {
948                inserted.push(FieldValue::value(json_to_value(json_val)));
949            }
950        }
951    }
952
953    // Commit once every object has been inserted: committing inside the loop
954    // would end the transaction, and the role set on it, after the first row.
955    conn.commit().await?;
956
957    // Return based on mutation type
958    match mutation_type {
959        MutationType::InsertOne => {
960            // Return single item
961            Ok(inserted.into_iter().next())
962        }
963        _ => {
964            // Return list
965            Ok(Some(FieldValue::list(inserted)))
966        }
967    }
968}
969
970/// Bind a JSON value to a sqlx query.
971fn bind_json_value<'q>(
972    query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
973    value: &serde_json::Value,
974) -> sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments> {
975    match value {
976        serde_json::Value::Null => query.bind(None::<String>),
977        serde_json::Value::Bool(b) => query.bind(*b),
978        serde_json::Value::Number(n) => {
979            if let Some(i) = n.as_i64() {
980                query.bind(i)
981            } else if let Some(f) = n.as_f64() {
982                query.bind(f)
983            } else {
984                query.bind(n.to_string())
985            }
986        }
987        serde_json::Value::String(s) => query.bind(s.clone()),
988        _ => query.bind(value.to_string()),
989    }
990}
991
992/// Execute an update mutation.
993async fn execute_update<'a>(
994    pool: &PgPool,
995    schema_name: &str,
996    table_name: &str,
997    role: &str,
998    set_value: serde_json::Value,
999    where_clause: Option<serde_json::Value>,
1000    mutation_type: MutationType,
1001) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
1002    use sqlx::Row;
1003
1004    trace!("Update mutation for {}: {:?}", table_name, set_value);
1005
1006    let set_map = match set_value {
1007        serde_json::Value::Object(map) => map,
1008        _ => return Err(async_graphql::Error::new("set must be an object")),
1009    };
1010
1011    if set_map.is_empty() {
1012        return Err(async_graphql::Error::new("set cannot be empty"));
1013    }
1014
1015    let mut conn = begin_with_role(pool, role).await?;
1016
1017    // Build SET clause
1018    let mut set_parts: Vec<String> = Vec::new();
1019    let mut param_idx = 1;
1020    for key in set_map.keys() {
1021        set_parts.push(format!(
1022            "{} = ${}",
1023            postrust_sql::escape_ident(key),
1024            param_idx
1025        ));
1026        param_idx += 1;
1027    }
1028
1029    // Build WHERE clause
1030    let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), param_idx)?;
1031
1032    // An absent or unrecognised `where` argument yields an empty clause, which
1033    // would update every row in the table. Refuse instead.
1034    if where_sql.is_empty() {
1035        return Err(async_graphql::Error::new(format!(
1036            "update on \"{}\" requires a `where` argument with at least one \
1037             recognised condition; refusing to update every row",
1038            table_name
1039        )));
1040    }
1041
1042    let sql = format!(
1043        "UPDATE {}.{} SET {} {} RETURNING row_to_json({}.{}.*)",
1044        postrust_sql::escape_ident(schema_name),
1045        postrust_sql::escape_ident(table_name),
1046        set_parts.join(", "),
1047        where_sql,
1048        postrust_sql::escape_ident(schema_name),
1049        postrust_sql::escape_ident(table_name)
1050    );
1051
1052    trace!("Executing UPDATE SQL: {}", sql);
1053
1054    // Build query with parameters
1055    let mut query = sqlx::query(&sql);
1056
1057    // Bind SET values
1058    for val in set_map.values() {
1059        query = bind_json_value(query, val);
1060    }
1061
1062    // Bind WHERE values
1063    for val in &where_values {
1064        query = bind_json_value(query, val);
1065    }
1066
1067    let rows = query.fetch_all(&mut *conn).await?;
1068
1069    let updated: Vec<FieldValue> = rows
1070        .iter()
1071        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
1072        .map(|v| FieldValue::value(json_to_value(v)))
1073        .collect();
1074
1075    // Return based on mutation type
1076    conn.commit().await?;
1077
1078    match mutation_type {
1079        MutationType::UpdateByPk => Ok(updated.into_iter().next()),
1080        _ => Ok(Some(FieldValue::list(updated))),
1081    }
1082}
1083
1084/// Execute a delete mutation.
1085async fn execute_delete<'a>(
1086    pool: &PgPool,
1087    schema_name: &str,
1088    table_name: &str,
1089    role: &str,
1090    where_clause: Option<serde_json::Value>,
1091    mutation_type: MutationType,
1092) -> Result<Option<FieldValue<'a>>, async_graphql::Error> {
1093    use sqlx::Row;
1094
1095    trace!("Delete mutation for {}", table_name);
1096
1097    let mut conn = begin_with_role(pool, role).await?;
1098
1099    // Build WHERE clause
1100    let (where_sql, where_values) = build_where_clause(where_clause.as_ref(), 1)?;
1101
1102    // An absent or unrecognised `where` argument yields an empty clause, which
1103    // would delete every row in the table. Refuse instead.
1104    if where_sql.is_empty() {
1105        return Err(async_graphql::Error::new(format!(
1106            "delete on \"{}\" requires a `where` argument with at least one \
1107             recognised condition; refusing to delete every row",
1108            table_name
1109        )));
1110    }
1111
1112    let sql = format!(
1113        "DELETE FROM {}.{} {} RETURNING row_to_json({}.{}.*)",
1114        postrust_sql::escape_ident(schema_name),
1115        postrust_sql::escape_ident(table_name),
1116        where_sql,
1117        postrust_sql::escape_ident(schema_name),
1118        postrust_sql::escape_ident(table_name)
1119    );
1120
1121    trace!("Executing DELETE SQL: {}", sql);
1122
1123    // Build query with parameters
1124    let mut query = sqlx::query(&sql);
1125
1126    // Bind WHERE values
1127    for val in &where_values {
1128        query = bind_json_value(query, val);
1129    }
1130
1131    let rows = query.fetch_all(&mut *conn).await?;
1132
1133    let deleted: Vec<FieldValue> = rows
1134        .iter()
1135        .filter_map(|row| row.try_get::<serde_json::Value, _>(0).ok())
1136        .map(|v| FieldValue::value(json_to_value(v)))
1137        .collect();
1138
1139    // Return based on mutation type
1140    conn.commit().await?;
1141
1142    match mutation_type {
1143        MutationType::DeleteByPk => Ok(deleted.into_iter().next()),
1144        _ => Ok(Some(FieldValue::list(deleted))),
1145    }
1146}
1147
1148/// Build a WHERE clause from a JSON filter object.
1149fn build_where_clause(
1150    where_value: Option<&serde_json::Value>,
1151    start_param_idx: usize,
1152) -> Result<(String, Vec<serde_json::Value>), async_graphql::Error> {
1153    let mut conditions: Vec<String> = Vec::new();
1154    let mut values: Vec<serde_json::Value> = Vec::new();
1155    let mut param_idx = start_param_idx;
1156
1157    if let Some(serde_json::Value::Object(map)) = where_value {
1158        for (key, val) in map {
1159            let column = postrust_sql::escape_ident(key);
1160
1161            match val {
1162                serde_json::Value::Object(op_map) => {
1163                    for (op, op_val) in op_map {
1164                        // Binary comparisons all bind exactly one parameter.
1165                        let binary_operator = match op.as_str() {
1166                            "eq" | "_eq" => Some("="),
1167                            "neq" | "_neq" => Some("!="),
1168                            "gt" | "_gt" => Some(">"),
1169                            "gte" | "_gte" => Some(">="),
1170                            "lt" | "_lt" => Some("<"),
1171                            "lte" | "_lte" => Some("<="),
1172                            "like" | "_like" => Some("LIKE"),
1173                            "ilike" | "_ilike" => Some("ILIKE"),
1174                            _ => None,
1175                        };
1176
1177                        if let Some(sql_operator) = binary_operator {
1178                            conditions.push(format!("{} {} ${}", column, sql_operator, param_idx));
1179                            values.push(op_val.clone());
1180                            param_idx += 1;
1181                            continue;
1182                        }
1183
1184                        match op.as_str() {
1185                            "is_null" | "_is_null" | "isNull" => {
1186                                if op_val.as_bool().unwrap_or(false) {
1187                                    conditions.push(format!("{} IS NULL", column));
1188                                } else {
1189                                    conditions.push(format!("{} IS NOT NULL", column));
1190                                }
1191                            }
1192                            "in" | "_in" => {
1193                                let items = op_val.as_array().ok_or_else(|| {
1194                                    async_graphql::Error::new(format!(
1195                                        "the `in` filter on \"{}\" requires a list of values",
1196                                        key
1197                                    ))
1198                                })?;
1199
1200                                if items.is_empty() {
1201                                    // `IN ()` is not valid SQL, and an empty set
1202                                    // matches nothing.
1203                                    conditions.push("false".to_string());
1204                                    continue;
1205                                }
1206
1207                                let mut placeholders = Vec::with_capacity(items.len());
1208                                for item in items {
1209                                    placeholders.push(format!("${}", param_idx));
1210                                    values.push(item.clone());
1211                                    param_idx += 1;
1212                                }
1213                                conditions.push(format!(
1214                                    "{} IN ({})",
1215                                    column,
1216                                    placeholders.join(", ")
1217                                ));
1218                            }
1219                            other => {
1220                                // Dropping an unrecognised operator would widen
1221                                // the result set -- returning every row for a
1222                                // query, or matching every row for a mutation.
1223                                // Fail loudly instead.
1224                                return Err(async_graphql::Error::new(format!(
1225                                    "unsupported filter operator \"{}\" on \"{}\"",
1226                                    other, key
1227                                )));
1228                            }
1229                        }
1230                    }
1231                }
1232                _ => {
1233                    // Direct equality: {field: value}
1234                    conditions.push(format!("{} = ${}", column, param_idx));
1235                    values.push(val.clone());
1236                    param_idx += 1;
1237                }
1238            }
1239        }
1240    }
1241
1242    let where_sql = if conditions.is_empty() {
1243        String::new()
1244    } else {
1245        format!("WHERE {}", conditions.join(" AND "))
1246    };
1247
1248    Ok((where_sql, values))
1249}
1250
1251/// Build an `ORDER BY` clause from the `orderBy` argument.
1252///
1253/// Entries are `column`, `column.asc` or `column.desc`. Column names are
1254/// checked against the table in the schema cache and then quoted, so a name
1255/// that is unknown -- or crafted to inject SQL -- is rejected rather than
1256/// interpolated. Returns an empty string when no ordering was requested.
1257async fn build_order_by_clause(
1258    ctx: &ResolverContext<'_>,
1259    schema_cache: &postrust_core::schema_cache::SchemaCacheRef,
1260    schema_name: &str,
1261    table_name: &str,
1262) -> Result<String, async_graphql::Error> {
1263    let Ok(order_arg) = ctx.args.try_get("orderBy") else {
1264        return Ok(String::new());
1265    };
1266
1267    let entries = match order_arg.list() {
1268        Ok(list) => list
1269            .iter()
1270            .map(|item| item.string().map(|s| s.to_string()))
1271            .collect::<Result<Vec<_>, _>>()
1272            .map_err(|_| async_graphql::Error::new("orderBy entries must be strings"))?,
1273        // A bare string is accepted as a single-column ordering.
1274        Err(_) => match order_arg.string() {
1275            Ok(single) => vec![single.to_string()],
1276            Err(_) => {
1277                return Err(async_graphql::Error::new(
1278                    "orderBy must be a string or a list of strings",
1279                ))
1280            }
1281        },
1282    };
1283
1284    if entries.is_empty() {
1285        return Ok(String::new());
1286    }
1287
1288    let guard = schema_cache
1289        .get()
1290        .await
1291        .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1292    let cache = guard
1293        .as_ref()
1294        .ok_or_else(|| async_graphql::Error::new("schema cache is not loaded"))?;
1295    let qi = postrust_core::api_request::QualifiedIdentifier::new(schema_name, table_name);
1296    let table = cache
1297        .get_table(&qi)
1298        .ok_or_else(|| async_graphql::Error::new(format!("unknown table \"{}\"", table_name)))?;
1299
1300    let mut terms = Vec::with_capacity(entries.len());
1301    for entry in entries {
1302        let (column, direction) = match entry.split_once('.') {
1303            Some((column, direction)) => (column, Some(direction)),
1304            None => (entry.as_str(), None),
1305        };
1306
1307        if table.get_column(column).is_none() {
1308            return Err(async_graphql::Error::new(format!(
1309                "cannot order by unknown column \"{}\" on \"{}\"",
1310                column, table_name
1311            )));
1312        }
1313
1314        let direction_sql = match direction.map(|d| d.to_ascii_lowercase()) {
1315            None => "",
1316            Some(d) if d == "asc" => " ASC",
1317            Some(d) if d == "desc" => " DESC",
1318            Some(other) => {
1319                return Err(async_graphql::Error::new(format!(
1320                    "invalid order direction \"{}\"; expected \"asc\" or \"desc\"",
1321                    other
1322                )))
1323            }
1324        };
1325
1326        terms.push(format!(
1327            "{}{}",
1328            postrust_sql::escape_ident(column),
1329            direction_sql
1330        ));
1331    }
1332
1333    Ok(format!(" ORDER BY {}", terms.join(", ")))
1334}
1335
1336/// Build the SELECT-list expressions that embed relationships in one query.
1337///
1338/// The GraphQL mirror of the REST builder: each requested relationship becomes a
1339/// correlated subselect yielding JSON, so the whole selection comes back from
1340/// the parent query instead of one query per relationship per level.
1341fn build_embed_expressions(
1342    schema_cache: &SchemaCache,
1343    relationships: &HashMap<String, Vec<RelationshipField>>,
1344    type_name: &str,
1345    parent_alias: &str,
1346    selection: async_graphql::SelectionField<'_>,
1347    max_rows: Option<i64>,
1348    alias_counter: &mut usize,
1349) -> Result<Vec<(String, String)>, async_graphql::Error> {
1350    let Some(available) = relationships.get(type_name) else {
1351        return Ok(Vec::new());
1352    };
1353
1354    let mut expressions = Vec::new();
1355
1356    for field in selection.selection_set() {
1357        let Some(rel) = available.iter().find(|r| r.name == field.name()) else {
1358            continue;
1359        };
1360
1361        let plan = postrust_core::embed::EmbedPlan::resolve(&rel.relationship, schema_cache)
1362            .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1363
1364        *alias_counter += 1;
1365        let child_alias = format!("e{}", alias_counter);
1366
1367        let nested = build_embed_expressions(
1368            schema_cache,
1369            relationships,
1370            &rel.target_type,
1371            &child_alias,
1372            field,
1373            max_rows,
1374            alias_counter,
1375        )?;
1376
1377        // Leaf fields are columns; anything that resolved to a relationship is
1378        // an expression instead.
1379        let child_relationships = relationships.get(&rel.target_type);
1380        let mut parts: Vec<String> = Vec::new();
1381        for sub in field.selection_set() {
1382            let name = sub.name();
1383            let is_relationship = child_relationships
1384                .map(|rels| rels.iter().any(|r| r.name == name))
1385                .unwrap_or(false);
1386            if !is_relationship {
1387                parts.push(postrust_sql::escape_ident(name));
1388            }
1389        }
1390        if parts.is_empty() && nested.is_empty() {
1391            parts.push(format!("{}.*", postrust_sql::escape_ident(&child_alias)));
1392        }
1393        for (field_name, expression) in nested {
1394            parts.push(format!(
1395                "{} AS {}",
1396                expression,
1397                postrust_sql::escape_ident(&field_name)
1398            ));
1399        }
1400
1401        let expression = plan
1402            .embed_expression(parent_alias, &child_alias, &parts.join(", "), max_rows)
1403            .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1404
1405        expressions.push((rel.name.clone(), expression));
1406    }
1407
1408    Ok(expressions)
1409}
1410
1411/// What embedding a relationship needs, independent of the rows involved.
1412///
1413/// The connection is passed alongside rather than held here: embedding runs on
1414/// the request's transaction, and a shared reference to this struct could not
1415/// hand out the mutable borrow the queries need.
1416struct EmbedContext<'c> {
1417    schema_cache: &'c SchemaCache,
1418    relationships: &'c HashMap<String, Vec<RelationshipField>>,
1419    max_rows: Option<i64>,
1420}
1421
1422/// Embed the relationship fields requested on `rows`.
1423///
1424/// One query per relationship per level, not one per row: the parents' join
1425/// keys are collected and passed as a single array. Recurses so a nested
1426/// selection costs one further query per relationship at each depth.
1427fn embed_relationships<'f>(
1428    conn: &'f mut sqlx::PgConnection,
1429    ctx: &'f EmbedContext<'f>,
1430    type_name: &'f str,
1431    selection: async_graphql::SelectionField<'f>,
1432    rows: &'f mut [serde_json::Value],
1433) -> futures::future::BoxFuture<'f, Result<(), async_graphql::Error>> {
1434    Box::pin(async move {
1435        if rows.is_empty() {
1436            return Ok(());
1437        }
1438
1439        let Some(available) = ctx.relationships.get(type_name) else {
1440            return Ok(());
1441        };
1442
1443        for requested in selection.selection_set() {
1444            let Some(rel) = available.iter().find(|r| r.name == requested.name()) else {
1445                continue;
1446            };
1447
1448            let plan =
1449                postrust_core::embed::EmbedPlan::resolve(&rel.relationship, ctx.schema_cache)
1450                    .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1451
1452            let keys = postrust_core::embed::parent_keys(rows, &plan.local_column);
1453
1454            // Project only what the selection asked for. A GraphQL selection
1455            // names its leaf fields, so the columns are known before the query
1456            // and an unrequested column need not be read, serialised, sent and
1457            // parsed just to be dropped.
1458            //
1459            // A nested relationship joins on a column of the child row, so that
1460            // column is added even when it was not selected; it is removed again
1461            // when the response is shaped. A selection whose sub-fields cannot
1462            // all be resolved to columns falls back to every column.
1463            let mut child_columns: Vec<String> = Vec::new();
1464            let mut project_everything = false;
1465            let child_relationships = ctx.relationships.get(&rel.target_type);
1466            for field in requested.selection_set() {
1467                let name = field.name();
1468                match child_relationships.and_then(|rels| rels.iter().find(|r| r.name == name)) {
1469                    Some(nested_rel) => {
1470                        match postrust_core::embed::EmbedPlan::resolve(
1471                            &nested_rel.relationship,
1472                            ctx.schema_cache,
1473                        ) {
1474                            Ok(nested_plan) => child_columns.push(nested_plan.local_column),
1475                            Err(_) => project_everything = true,
1476                        }
1477                    }
1478                    None => child_columns.push(name.to_string()),
1479                }
1480            }
1481            if project_everything || child_columns.is_empty() {
1482                child_columns.clear();
1483            }
1484
1485            let mut grouped = if keys.is_empty() {
1486                std::collections::HashMap::new()
1487            } else {
1488                let sql = plan
1489                    .children_grouped_sql(ctx.max_rows, &child_columns)
1490                    .map_err(|e| async_graphql::Error::new(e.to_string()))?;
1491
1492                let fetched = sqlx::query(&sql).bind(&keys).fetch_all(&mut *conn).await?;
1493
1494                // The query returns the join key and a JSON array of that key's
1495                // children, grouped by PostgreSQL rather than row by row here.
1496                use sqlx::Row;
1497                let pairs: Vec<(serde_json::Value, serde_json::Value)> = fetched
1498                    .into_iter()
1499                    .filter_map(|row| {
1500                        Some((
1501                            row.try_get::<serde_json::Value, _>(0).ok()?,
1502                            row.try_get::<serde_json::Value, _>(1).ok()?,
1503                        ))
1504                    })
1505                    .collect();
1506
1507                postrust_core::embed::group_from_aggregated(pairs)
1508            };
1509
1510            // Recurse before attaching, so nested embeds land in the values
1511            // copied onto the parents. One query serves every child row at this
1512            // level, so the rows are flattened for the call and put back
1513            // afterwards; skipped when the selection asks for nothing deeper.
1514            let has_deeper_embed = ctx
1515                .relationships
1516                .get(&rel.target_type)
1517                .map(|rels| {
1518                    requested
1519                        .selection_set()
1520                        .any(|field| rels.iter().any(|r| r.name == field.name()))
1521                })
1522                .unwrap_or(false);
1523
1524            if has_deeper_embed {
1525                let mut order: Vec<(String, usize)> = Vec::with_capacity(grouped.len());
1526                let mut flat: Vec<serde_json::Value> = Vec::new();
1527                for (key, children) in grouped.drain() {
1528                    order.push((key, children.len()));
1529                    flat.extend(children);
1530                }
1531
1532                embed_relationships(&mut *conn, ctx, &rel.target_type, requested, &mut flat)
1533                    .await?;
1534
1535                let mut rest = flat.into_iter();
1536                for (key, count) in order {
1537                    grouped.insert(key, rest.by_ref().take(count).collect());
1538                }
1539            }
1540
1541            for row in rows.iter_mut() {
1542                postrust_core::embed::attach_to_parent(row, &rel.name, &plan, &grouped);
1543            }
1544        }
1545
1546        Ok(())
1547    })
1548}
1549
1550/// Build a `where` document that addresses exactly one row by primary key.
1551///
1552/// Used by the by-PK mutations, which take the key columns as arguments instead
1553/// of a free-form `where`.
1554fn pk_where_from_args(
1555    ctx: &ResolverContext<'_>,
1556    table_name: &str,
1557    pk_columns: &[(String, String)],
1558) -> Result<serde_json::Value, async_graphql::Error> {
1559    if pk_columns.is_empty() {
1560        return Err(async_graphql::Error::new(format!(
1561            "\"{}\" has no primary key, so it cannot be mutated by key",
1562            table_name
1563        )));
1564    }
1565
1566    let mut conditions = serde_json::Map::new();
1567    for (col_name, _) in pk_columns {
1568        let value = ctx.args.try_get(col_name).map_err(|_| {
1569            async_graphql::Error::new(format!(
1570                "missing required primary key argument \"{}\"",
1571                col_name
1572            ))
1573        })?;
1574        conditions.insert(
1575            col_name.clone(),
1576            serde_json::json!({ "eq": accessor_to_json(&value) }),
1577        );
1578    }
1579
1580    Ok(serde_json::Value::Object(conditions))
1581}
1582
1583/// GraphQL scalar name to use for a primary key argument of the given
1584/// PostgreSQL type.
1585///
1586/// Falls back to `String` for anything that does not map to a plain scalar --
1587/// a composite or array key cannot be expressed as a single named argument, and
1588/// the value is cast to the column's type in SQL anyway.
1589fn pk_argument_type(pg_type: &str) -> String {
1590    let rendered = crate::types::pg_type_to_graphql(pg_type).to_string();
1591    if rendered.starts_with('[') {
1592        "String".to_string()
1593    } else {
1594        rendered
1595    }
1596}
1597
1598/// Convert a GraphQL type string to a TypeRef.
1599fn graphql_type_ref(type_str: &str) -> TypeRef {
1600    // Parse type string like "[Users!]!" or "String" or "Int!"
1601    let is_list = type_str.starts_with('[');
1602    let is_nn = type_str.ends_with('!');
1603
1604    // Strip outer modifiers: first the trailing !, then the brackets
1605    let inner = if is_list {
1606        let stripped = type_str
1607            .trim_end_matches('!') // Remove outer !
1608            .trim_start_matches('[') // Remove [
1609            .trim_end_matches(']'); // Remove ]
1610        stripped
1611    } else {
1612        type_str.trim_end_matches('!')
1613    };
1614
1615    let inner_nn = inner.ends_with('!');
1616    let base_type = inner.trim_end_matches('!');
1617
1618    if is_list {
1619        if is_nn {
1620            if inner_nn {
1621                TypeRef::named_nn_list_nn(base_type)
1622            } else {
1623                TypeRef::named_list_nn(base_type)
1624            }
1625        } else if inner_nn {
1626            TypeRef::named_nn_list(base_type)
1627        } else {
1628            TypeRef::named_list(base_type)
1629        }
1630    } else if is_nn {
1631        TypeRef::named_nn(base_type)
1632    } else {
1633        TypeRef::named(base_type)
1634    }
1635}
1636
1637/// Convert ValueAccessor to JSON.
1638fn accessor_to_json(accessor: &ValueAccessor<'_>) -> serde_json::Value {
1639    // Use the deserialize method if available, or convert manually
1640    if accessor.is_null() {
1641        serde_json::Value::Null
1642    } else if let Ok(b) = accessor.boolean() {
1643        serde_json::Value::Bool(b)
1644    } else if let Ok(i) = accessor.i64() {
1645        serde_json::Value::Number(i.into())
1646    } else if let Ok(f) = accessor.f64() {
1647        serde_json::Number::from_f64(f)
1648            .map(serde_json::Value::Number)
1649            .unwrap_or(serde_json::Value::Null)
1650    } else if let Ok(s) = accessor.string() {
1651        serde_json::Value::String(s.to_string())
1652    } else if let Ok(list) = accessor.list() {
1653        serde_json::Value::Array(list.iter().map(|v| accessor_to_json(&v)).collect())
1654    } else if let Ok(obj) = accessor.object() {
1655        let map: serde_json::Map<String, serde_json::Value> = obj
1656            .iter()
1657            .map(|(k, v)| (k.to_string(), accessor_to_json(&v)))
1658            .collect();
1659        serde_json::Value::Object(map)
1660    } else {
1661        serde_json::Value::Null
1662    }
1663}
1664
1665/// Convert async-graphql Value to JSON.
1666#[allow(dead_code)]
1667fn value_to_json(value: &Value) -> serde_json::Value {
1668    match value {
1669        Value::Null => serde_json::Value::Null,
1670        Value::Boolean(b) => serde_json::Value::Bool(*b),
1671        Value::Number(n) => {
1672            if let Some(i) = n.as_i64() {
1673                serde_json::Value::Number(i.into())
1674            } else if let Some(f) = n.as_f64() {
1675                serde_json::Value::Number(serde_json::Number::from_f64(f).unwrap())
1676            } else {
1677                serde_json::Value::Null
1678            }
1679        }
1680        Value::String(s) => serde_json::Value::String(s.clone()),
1681        Value::List(arr) => serde_json::Value::Array(arr.iter().map(value_to_json).collect()),
1682        Value::Object(obj) => {
1683            let map: serde_json::Map<String, serde_json::Value> = obj
1684                .iter()
1685                .map(|(k, v)| (k.to_string(), value_to_json(v)))
1686                .collect();
1687            serde_json::Value::Object(map)
1688        }
1689        Value::Binary(b) => serde_json::Value::String(base64::Engine::encode(
1690            &base64::engine::general_purpose::STANDARD,
1691            b,
1692        )),
1693        Value::Enum(e) => serde_json::Value::String(e.to_string()),
1694    }
1695}
1696
1697/// Convert JSON to async-graphql Value.
1698fn json_to_value(json: serde_json::Value) -> Value {
1699    match json {
1700        serde_json::Value::Null => Value::Null,
1701        serde_json::Value::Bool(b) => Value::Boolean(b),
1702        serde_json::Value::Number(n) => {
1703            if let Some(i) = n.as_i64() {
1704                Value::Number(i.into())
1705            } else if let Some(f) = n.as_f64() {
1706                Value::Number(async_graphql::Number::from_f64(f).unwrap())
1707            } else {
1708                Value::Null
1709            }
1710        }
1711        serde_json::Value::String(s) => Value::String(s),
1712        serde_json::Value::Array(arr) => Value::List(arr.into_iter().map(json_to_value).collect()),
1713        serde_json::Value::Object(obj) => {
1714            let map: indexmap::IndexMap<async_graphql::Name, Value> = obj
1715                .into_iter()
1716                .map(|(k, v)| (async_graphql::Name::new(k), json_to_value(v)))
1717                .collect();
1718            Value::Object(map)
1719        }
1720    }
1721}
1722
1723/// Create BigInt scalar type.
1724fn create_bigint_scalar() -> Scalar {
1725    Scalar::new("BigInt")
1726        .description("64-bit integer")
1727        .specified_by_url("https://spec.graphql.org/draft/#sec-Int")
1728}
1729
1730/// Create BigDecimal scalar type.
1731fn create_bigdecimal_scalar() -> Scalar {
1732    Scalar::new("BigDecimal").description("Arbitrary precision decimal number")
1733}
1734
1735/// Create JSON scalar type.
1736fn create_json_scalar() -> Scalar {
1737    Scalar::new("JSON")
1738        .description("Arbitrary JSON value")
1739        .specified_by_url("https://spec.graphql.org/draft/#sec-Scalars")
1740}
1741
1742/// Create UUID scalar type.
1743fn create_uuid_scalar() -> Scalar {
1744    Scalar::new("UUID").description("UUID string")
1745}
1746
1747/// Create Date scalar type.
1748fn create_date_scalar() -> Scalar {
1749    Scalar::new("Date").description("ISO 8601 date string (YYYY-MM-DD)")
1750}
1751
1752/// Create DateTime scalar type.
1753fn create_datetime_scalar() -> Scalar {
1754    Scalar::new("DateTime").description("ISO 8601 datetime string")
1755}
1756
1757/// Create Time scalar type.
1758fn create_time_scalar() -> Scalar {
1759    Scalar::new("Time").description("ISO 8601 time string (HH:MM:SS)")
1760}
1761
1762/// Register filter input types.
1763///
1764/// These are currently unreachable: the `filter` and `where` arguments are
1765/// declared as the `JSON` scalar, so no field references these input objects
1766/// and async-graphql prunes them from the published schema (introspecting
1767/// `IntFilterInput` returns null). They are kept as the shape to move to if
1768/// filters become typed per column; until then the operators a filter actually
1769/// supports are the ones `build_where_clause` implements, and it rejects
1770/// anything else rather than ignoring it.
1771fn register_filter_input_types(builder: SchemaBuilder) -> SchemaBuilder {
1772    let string_filter = InputObject::new("StringFilterInput")
1773        .field(InputValue::new("eq", TypeRef::named("String")))
1774        .field(InputValue::new("neq", TypeRef::named("String")))
1775        .field(InputValue::new("like", TypeRef::named("String")))
1776        .field(InputValue::new("ilike", TypeRef::named("String")))
1777        .field(InputValue::new("in", TypeRef::named_list("String")))
1778        .field(InputValue::new("isNull", TypeRef::named("Boolean")));
1779
1780    let int_filter = InputObject::new("IntFilterInput")
1781        .field(InputValue::new("eq", TypeRef::named("Int")))
1782        .field(InputValue::new("neq", TypeRef::named("Int")))
1783        .field(InputValue::new("gt", TypeRef::named("Int")))
1784        .field(InputValue::new("gte", TypeRef::named("Int")))
1785        .field(InputValue::new("lt", TypeRef::named("Int")))
1786        .field(InputValue::new("lte", TypeRef::named("Int")))
1787        .field(InputValue::new("in", TypeRef::named_list("Int")));
1788
1789    let boolean_filter = InputObject::new("BooleanFilterInput")
1790        .field(InputValue::new("eq", TypeRef::named("Boolean")));
1791
1792    builder
1793        .register(string_filter)
1794        .register(int_filter)
1795        .register(boolean_filter)
1796}
1797
1798#[cfg(test)]
1799mod tests {
1800    use super::*;
1801    use indexmap::IndexMap;
1802    use postrust_core::schema_cache::{Column, Table};
1803    use std::collections::{HashMap, HashSet};
1804
1805    fn create_test_table(name: &str) -> Table {
1806        let mut columns = IndexMap::new();
1807        columns.insert(
1808            "id".into(),
1809            Column {
1810                name: "id".into(),
1811                description: None,
1812                nullable: false,
1813                data_type: "integer".into(),
1814                nominal_type: "int4".into(),
1815                max_len: None,
1816                default: Some("nextval('id_seq')".into()),
1817                enum_values: vec![],
1818                is_pk: true,
1819                position: 1,
1820            },
1821        );
1822        columns.insert(
1823            "name".into(),
1824            Column {
1825                name: "name".into(),
1826                description: None,
1827                nullable: false,
1828                data_type: "text".into(),
1829                nominal_type: "text".into(),
1830                max_len: None,
1831                default: None,
1832                enum_values: vec![],
1833                is_pk: false,
1834                position: 2,
1835            },
1836        );
1837
1838        Table {
1839            schema: "public".into(),
1840            name: name.into(),
1841            description: None,
1842            is_view: false,
1843            insertable: true,
1844            updatable: true,
1845            deletable: true,
1846            pk_cols: vec!["id".into()],
1847            columns,
1848        }
1849    }
1850
1851    fn create_test_schema_cache() -> SchemaCache {
1852        let mut tables = HashMap::new();
1853        let users = create_test_table("users");
1854        tables.insert(users.qualified_identifier(), users);
1855
1856        SchemaCache {
1857            tables,
1858            relationships: HashMap::new(),
1859            routines: HashMap::new(),
1860            timezones: HashSet::new(),
1861            pg_version: 150000,
1862        }
1863    }
1864
1865    // ============================================================================
1866    // Type Reference Tests
1867    // ============================================================================
1868
1869    #[test]
1870    fn test_graphql_type_ref_simple() {
1871        let _type_ref = graphql_type_ref("String");
1872        // TypeRef doesn't implement PartialEq, so we just test it doesn't panic
1873    }
1874
1875    #[test]
1876    fn test_graphql_type_ref_non_null() {
1877        let _type_ref = graphql_type_ref("String!");
1878    }
1879
1880    #[test]
1881    fn test_graphql_type_ref_list() {
1882        let _type_ref = graphql_type_ref("[String]");
1883    }
1884
1885    #[test]
1886    fn test_graphql_type_ref_list_non_null() {
1887        let _type_ref = graphql_type_ref("[String!]!");
1888    }
1889
1890    // ============================================================================
1891    // Value Conversion Tests
1892    // ============================================================================
1893
1894    #[test]
1895    fn test_value_to_json_null() {
1896        let value = Value::Null;
1897        let json = value_to_json(&value);
1898        assert_eq!(json, serde_json::Value::Null);
1899    }
1900
1901    #[test]
1902    fn test_value_to_json_boolean() {
1903        let value = Value::Boolean(true);
1904        let json = value_to_json(&value);
1905        assert_eq!(json, serde_json::Value::Bool(true));
1906    }
1907
1908    #[test]
1909    fn test_value_to_json_number() {
1910        let value = Value::Number(42.into());
1911        let json = value_to_json(&value);
1912        assert_eq!(json, serde_json::json!(42));
1913    }
1914
1915    #[test]
1916    fn test_value_to_json_string() {
1917        let value = Value::String("hello".to_string());
1918        let json = value_to_json(&value);
1919        assert_eq!(json, serde_json::Value::String("hello".to_string()));
1920    }
1921
1922    #[test]
1923    fn test_value_to_json_list() {
1924        let value = Value::List(vec![Value::Number(1.into()), Value::Number(2.into())]);
1925        let json = value_to_json(&value);
1926        assert_eq!(json, serde_json::json!([1, 2]));
1927    }
1928
1929    #[test]
1930    fn test_json_to_value_null() {
1931        let json = serde_json::Value::Null;
1932        let value = json_to_value(json);
1933        assert!(matches!(value, Value::Null));
1934    }
1935
1936    #[test]
1937    fn test_json_to_value_boolean() {
1938        let json = serde_json::Value::Bool(false);
1939        let value = json_to_value(json);
1940        assert!(matches!(value, Value::Boolean(false)));
1941    }
1942
1943    #[test]
1944    fn test_json_to_value_number() {
1945        let json = serde_json::json!(123);
1946        let value = json_to_value(json);
1947        assert!(matches!(value, Value::Number(_)));
1948    }
1949
1950    #[test]
1951    fn test_json_to_value_string() {
1952        let json = serde_json::Value::String("test".to_string());
1953        let value = json_to_value(json);
1954        assert!(matches!(value, Value::String(_)));
1955    }
1956
1957    #[test]
1958    fn test_json_to_value_array() {
1959        let json = serde_json::json!([1, 2, 3]);
1960        let value = json_to_value(json);
1961        assert!(matches!(value, Value::List(_)));
1962    }
1963
1964    #[test]
1965    fn test_json_to_value_object() {
1966        let json = serde_json::json!({"key": "value"});
1967        let value = json_to_value(json);
1968        assert!(matches!(value, Value::Object(_)));
1969    }
1970
1971    // ============================================================================
1972    // Schema Building Tests
1973    // ============================================================================
1974
1975    #[test]
1976    fn test_build_dynamic_schema() {
1977        let cache = create_test_schema_cache();
1978        let config = SchemaConfig::default();
1979        let generated = build_schema(&cache, &config);
1980
1981        let result = build_dynamic_schema(&generated, &cache, None, None);
1982        if let Err(ref e) = result {
1983            eprintln!("Schema build error: {:?}", e);
1984        }
1985        assert!(result.is_ok(), "Schema build failed: {:?}", result.err());
1986    }
1987
1988    #[test]
1989    fn test_create_object_type() {
1990        let table = create_test_table("users");
1991        let obj = TableObjectType::from_table(&table);
1992        let _gql_obj = create_object_type(&obj, &[]);
1993    }
1994
1995    #[test]
1996    fn test_create_query_type() {
1997        let cache = create_test_schema_cache();
1998        let config = SchemaConfig::default();
1999        let generated = build_schema(&cache, &config);
2000
2001        let _query = create_query_type(&generated, None, Arc::new(HashMap::new()));
2002    }
2003
2004    #[test]
2005    fn test_create_mutation_type() {
2006        let cache = create_test_schema_cache();
2007        let config = SchemaConfig::default();
2008        let generated = build_schema(&cache, &config);
2009
2010        let _mutation = create_mutation_type(&generated);
2011    }
2012
2013    // ============================================================================
2014    // Scalar Tests
2015    // ============================================================================
2016
2017    #[test]
2018    fn test_create_scalars() {
2019        let _bigint = create_bigint_scalar();
2020        let _json = create_json_scalar();
2021        let _uuid = create_uuid_scalar();
2022        let _datetime = create_datetime_scalar();
2023    }
2024
2025    // ============================================================================
2026    // Filter Input Type Tests
2027    // ============================================================================
2028
2029    #[test]
2030    fn test_register_filter_input_types() {
2031        let cache = create_test_schema_cache();
2032        let config = SchemaConfig::default();
2033        let _generated = build_schema(&cache, &config);
2034
2035        // Build a minimal schema with filter types
2036        let query =
2037            Object::new("Query").field(Field::new("test", TypeRef::named("String"), |_| {
2038                FieldFuture::new(async { Ok(None::<FieldValue>) })
2039            }));
2040
2041        let mut builder = Schema::build("Query", None::<&str>, None);
2042        builder = builder.register(query);
2043        builder = register_filter_input_types(builder);
2044
2045        let result = builder.finish();
2046        assert!(result.is_ok());
2047    }
2048
2049    // ============================================================================
2050    // Subscription Tests
2051    // ============================================================================
2052
2053    #[test]
2054    fn test_build_schema_with_subscriptions() {
2055        let cache = create_test_schema_cache();
2056        let config = SchemaConfig {
2057            enable_subscriptions: true,
2058            ..SchemaConfig::default()
2059        };
2060        let generated = build_schema(&cache, &config);
2061
2062        // Generate subscription fields
2063        let sub_fields = generate_subscription_fields(&cache, &generated);
2064        assert!(!sub_fields.is_empty(), "Should have subscription fields");
2065
2066        // Build schema with subscriptions
2067        let result = build_dynamic_schema(&generated, &cache, Some(&sub_fields), None);
2068        assert!(result.is_ok(), "Schema with subscriptions should build");
2069    }
2070
2071    #[test]
2072    fn test_subscription_field_generation() {
2073        let cache = create_test_schema_cache();
2074        let config = SchemaConfig::default();
2075        let generated = build_schema(&cache, &config);
2076
2077        let fields = generate_subscription_fields(&cache, &generated);
2078
2079        // Should have one subscription field for the users table
2080        assert_eq!(fields.len(), 1);
2081        assert_eq!(fields[0].name, "users");
2082        assert_eq!(fields[0].table_name, "users");
2083        assert_eq!(fields[0].channel_name(), "postrust_public_users");
2084    }
2085
2086    #[test]
2087    fn test_create_subscription_type() {
2088        use crate::subscription::SubscriptionField as SubField;
2089
2090        let fields = vec![
2091            SubField::for_table("public", "users", "Users"),
2092            SubField::for_table("public", "orders", "Orders"),
2093        ];
2094
2095        let _subscription = create_subscription_type(&fields);
2096        // Just test that it doesn't panic
2097    }
2098}