1use crate::error::{Error, Result};
19use crate::schema_cache::{Relationship, SchemaCache, Table};
20use std::collections::HashMap;
21
22#[derive(Clone, Debug)]
24pub struct EmbedPlan {
25 pub local_column: String,
27 pub foreign_column: String,
29 pub foreign_column_type: String,
31 pub foreign_schema: String,
33 pub foreign_table: String,
35 pub is_list: bool,
37 pub columns: Vec<(String, String)>,
43 pub junction: Option<EmbedJunction>,
48 pub function: Option<crate::api_request::QualifiedIdentifier>,
55 pub row_argument: Option<String>,
58}
59
60#[derive(Clone, Debug)]
62pub struct EmbedJunction {
63 pub schema: String,
64 pub table: String,
65 pub parent_columns: Vec<(String, String)>,
72 pub child_columns: Vec<(String, String)>,
74}
75
76impl EmbedJunction {
77 fn child_join(&self, junction_alias: &str, child_alias: &str) -> String {
79 Self::equalities(&self.child_columns, junction_alias, child_alias, true)
80 }
81
82 fn parent_correlation(&self, junction_alias: &str, parent_alias: &str) -> String {
84 Self::equalities(&self.parent_columns, junction_alias, parent_alias, false)
85 }
86
87 fn equalities(
93 pairs: &[(String, String)],
94 junction_alias: &str,
95 other_alias: &str,
96 junction_first: bool,
97 ) -> String {
98 pairs
99 .iter()
100 .map(|(left, right)| {
101 let (junction_column, other_column) = match junction_first {
102 true => (left, right),
103 false => (right, left),
104 };
105 format!(
106 "{}.{} = {}.{}",
107 postrust_sql::escape_ident(junction_alias),
108 postrust_sql::escape_ident(junction_column),
109 postrust_sql::escape_ident(other_alias),
110 postrust_sql::escape_ident(other_column),
111 )
112 })
113 .collect::<Vec<_>>()
114 .join(" AND ")
115 }
116}
117
118impl EmbedPlan {
119 pub fn resolve(relationship: &Relationship, schema_cache: &SchemaCache) -> Result<Self> {
124 let foreign_table_qi = relationship.foreign_table().clone();
125
126 if let Relationship::Computed {
129 function,
130 foreign_table,
131 to_one,
132 row_argument,
133 ..
134 } = relationship
135 {
136 return Ok(Self {
137 local_column: String::new(),
138 foreign_column: String::new(),
139 foreign_column_type: String::new(),
140 foreign_schema: foreign_table.schema.clone(),
141 foreign_table: foreign_table.name.clone(),
142 is_list: !to_one,
143 columns: Vec::new(),
144 junction: None,
145 function: Some(function.clone()),
146 row_argument: row_argument.clone(),
147 });
148 }
149
150 if let Relationship::ForeignKey {
151 cardinality: crate::schema_cache::Cardinality::M2M(junction),
152 ..
153 } = relationship
154 {
155 let (local_column, _) =
160 junction.source_columns.first().cloned().ok_or_else(|| {
161 Error::EmbeddingError("junction has no source columns".into())
162 })?;
163 let (_, foreign_column) =
164 junction.target_columns.first().cloned().ok_or_else(|| {
165 Error::EmbeddingError("junction has no target columns".into())
166 })?;
167
168 return Ok(Self {
169 local_column,
170 foreign_column,
171 foreign_column_type: String::new(),
172 foreign_schema: foreign_table_qi.schema.clone(),
173 foreign_table: foreign_table_qi.name.clone(),
174 is_list: true,
176 columns: Vec::new(),
177 row_argument: None,
178 junction: Some(EmbedJunction {
179 schema: junction.table.schema.clone(),
180 table: junction.table.name.clone(),
181 parent_columns: junction.source_columns.clone(),
182 child_columns: junction.target_columns.clone(),
183 }),
184 function: None,
185 });
186 }
187
188 let columns = match relationship {
189 Relationship::ForeignKey { cardinality, .. } => cardinality.columns(),
190 Relationship::Computed { .. } => unreachable!("handled above"),
191 };
192
193 let (local_column, foreign_column) = columns
194 .first()
195 .cloned()
196 .ok_or_else(|| Error::EmbeddingError("relationship joins on no columns".into()))?;
197
198 let foreign_table: &Table = schema_cache.get_table(&foreign_table_qi).ok_or_else(|| {
199 Error::EmbeddingError(format!(
200 "cannot embed \"{}\": it is not in an exposed schema",
201 foreign_table_qi
202 ))
203 })?;
204
205 let foreign_column_type = foreign_table
206 .get_column(&foreign_column)
207 .map(|c| c.nominal_type.clone())
208 .ok_or_else(|| {
209 Error::EmbeddingError(format!(
210 "cannot embed \"{}\": join column \"{}\" not found",
211 foreign_table_qi, foreign_column
212 ))
213 })?;
214
215 Ok(Self {
216 local_column,
217 foreign_column,
218 foreign_column_type,
219 foreign_schema: foreign_table_qi.schema.clone(),
220 foreign_table: foreign_table_qi.name.clone(),
221 is_list: !relationship.is_to_one(),
222 columns,
223 junction: None,
224 function: None,
225 row_argument: None,
226 })
227 }
228
229 fn correlation(&self, parent_alias: &str, child_alias: &str) -> String {
234 let pairs = if self.columns.is_empty() {
235 vec![(self.local_column.clone(), self.foreign_column.clone())]
236 } else {
237 self.columns.clone()
238 };
239
240 pairs
241 .iter()
242 .map(|(local, foreign)| {
243 format!(
244 "{}.{} = {}.{}",
245 postrust_sql::escape_ident(child_alias),
246 postrust_sql::escape_ident(foreign),
247 postrust_sql::escape_ident(parent_alias),
248 postrust_sql::escape_ident(local),
249 )
250 })
251 .collect::<Vec<_>>()
252 .join(" AND ")
253 }
254
255 pub fn children_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
269 let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
270 Error::EmbeddingError(format!(
271 "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
272 self.foreign_table, self.foreign_column_type
273 ))
274 })?;
275
276 let projection = self.projection(columns);
277
278 let mut inner = format!(
279 "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
280 projection,
281 postrust_sql::escape_ident(&self.foreign_schema),
282 postrust_sql::escape_ident(&self.foreign_table),
283 postrust_sql::escape_ident(&self.foreign_column),
284 type_name
285 );
286
287 if let Some(limit) = limit {
288 inner.push_str(&format!(" LIMIT {}", limit));
289 }
290
291 Ok(format!("SELECT row_to_json(t) FROM ({}) t", inner))
292 }
293
294 pub fn children_grouped_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
309 if self
310 .junction
311 .as_ref()
312 .is_some_and(|j| j.parent_columns.len() > 1)
313 {
314 return Err(Error::EmbeddingError(format!(
315 "embedding \"{}\" this way is not supported: it joins through a junction \
316 keyed on several columns, and grouping children by key needs a single one",
317 self.foreign_table,
318 )));
319 }
320
321 if self.columns.len() > 1 {
322 return Err(Error::EmbeddingError(format!(
323 "embedding \"{}\" this way is not supported: it joins on {} columns, and \
324 grouping children by key needs a single one",
325 self.foreign_table,
326 self.columns.len()
327 )));
328 }
329
330 let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
331 Error::EmbeddingError(format!(
332 "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
333 self.foreign_table, self.foreign_column_type
334 ))
335 })?;
336
337 let key = postrust_sql::escape_ident(&self.foreign_column);
338
339 let mut inner = format!(
340 "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
341 self.projection(columns),
342 postrust_sql::escape_ident(&self.foreign_schema),
343 postrust_sql::escape_ident(&self.foreign_table),
344 key,
345 type_name
346 );
347
348 if let Some(limit) = limit {
349 inner.push_str(&format!(" LIMIT {}", limit));
350 }
351
352 Ok(format!(
353 "SELECT to_jsonb(c.{key}) AS k, json_agg(row_to_json(c)) AS v \
354 FROM ({inner}) c GROUP BY c.{key}",
355 key = key,
356 inner = inner
357 ))
358 }
359
360 #[allow(clippy::too_many_arguments)] pub fn correlated_rows(
377 &self,
378 parent_alias: &str,
379 parent_row: &str,
380 child_alias: &str,
381 inner_select: &str,
382 limit: Option<i64>,
383 offset: i64,
387 child_where: Option<&str>,
388 order_by: Option<&str>,
392 function_arguments: Option<&str>,
395 ) -> Result<String> {
396 let mut inner = match &self.function {
404 Some(function) => format!(
405 "SELECT {} FROM {}.{}({}) AS {} WHERE true",
406 inner_select,
407 postrust_sql::escape_ident(&function.schema),
408 postrust_sql::escape_ident(&function.name),
409 function_arguments.unwrap_or(parent_row),
414 postrust_sql::escape_ident(child_alias),
415 ),
416 None => match &self.junction {
417 Some(junction) => {
420 let junction_alias = format!("{}_j", child_alias);
421 format!(
422 "SELECT {} FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
423 inner_select,
424 postrust_sql::escape_ident(&self.foreign_schema),
425 postrust_sql::escape_ident(&self.foreign_table),
426 postrust_sql::escape_ident(child_alias),
427 postrust_sql::escape_ident(&junction.schema),
428 postrust_sql::escape_ident(&junction.table),
429 postrust_sql::escape_ident(&junction_alias),
430 junction.child_join(&junction_alias, child_alias),
431 junction.parent_correlation(&junction_alias, parent_alias),
432 )
433 }
434 None => format!(
435 "SELECT {} FROM {}.{} AS {} WHERE {}",
436 inner_select,
437 postrust_sql::escape_ident(&self.foreign_schema),
438 postrust_sql::escape_ident(&self.foreign_table),
439 postrust_sql::escape_ident(child_alias),
440 self.correlation(parent_alias, child_alias),
441 ),
442 },
443 };
444
445 if let Some(child_where) = child_where {
449 inner.push_str(" AND ");
450 inner.push_str(child_where);
451 }
452
453 if let Some(order_by) = order_by {
454 inner.push_str(" ORDER BY ");
455 inner.push_str(order_by);
456 }
457
458 if let Some(limit) = limit {
463 inner.push_str(&format!(" LIMIT {}", limit));
464 } else if !self.is_list {
465 inner.push_str(" LIMIT 1");
466 }
467
468 if offset > 0 {
469 inner.push_str(&format!(" OFFSET {}", offset));
470 }
471
472 Ok(inner)
473 }
474
475 #[allow(clippy::too_many_arguments)] pub fn embed_expression(
482 &self,
483 parent_alias: &str,
484 parent_row: &str,
485 child_alias: &str,
486 inner_select: &str,
487 limit: Option<i64>,
488 offset: i64,
489 child_where: Option<&str>,
490 order_by: Option<&str>,
491 function_arguments: Option<&str>,
492 ) -> Result<String> {
493 let inner = self.correlated_rows(
494 parent_alias,
495 parent_row,
496 child_alias,
497 inner_select,
498 limit,
499 offset,
500 child_where,
501 order_by,
502 function_arguments,
503 )?;
504
505 let alias = postrust_sql::escape_ident(&format!("{}_j", child_alias));
506
507 Ok(if self.is_list {
513 format!(
516 "COALESCE((SELECT json_agg(row_to_json({alias})) FROM ({inner}) {alias}), '[]'::json)::jsonb",
517 alias = alias,
518 inner = inner
519 )
520 } else {
521 format!(
522 "(SELECT row_to_json({alias}) FROM ({inner}) {alias})::jsonb",
523 alias = alias,
524 inner = inner
525 )
526 })
527 }
528
529 #[allow(clippy::too_many_arguments)] pub fn aggregate_expression(
545 &self,
546 parent_alias: &str,
547 parent_row: &str,
548 child_alias: &str,
549 aggregate_select: &str,
550 limit: Option<i64>,
551 offset: i64,
552 child_where: Option<&str>,
553 order_by: Option<&str>,
554 function_arguments: Option<&str>,
555 distinct_on: &str,
559 ) -> Result<String> {
560 let child = postrust_sql::escape_ident(child_alias);
561 let rows = self.correlated_rows(
562 parent_alias,
563 parent_row,
564 child_alias,
565 &format!("{}{}.*", distinct_on, child),
566 limit,
567 offset,
568 child_where,
569 order_by,
570 function_arguments,
571 )?;
572 let alias = postrust_sql::escape_ident(&format!("{}_a", child_alias));
573 Ok(format!(
574 "(SELECT row_to_json({alias}) FROM \
575 (SELECT {select} FROM ({rows}) AS {child}) {alias})::jsonb",
576 alias = alias,
577 select = aggregate_select,
578 rows = rows,
579 child = child
580 ))
581 }
582
583 pub fn order_expression(
591 &self,
592 parent_alias: &str,
593 parent_row: &str,
594 child_alias: &str,
595 column_sql: &str,
596 ) -> String {
597 let source = match (&self.function, &self.junction) {
598 (Some(function), _) => format!(
599 "{}.{}({}) AS {} WHERE true",
600 postrust_sql::escape_ident(&function.schema),
601 postrust_sql::escape_ident(&function.name),
602 parent_row,
603 postrust_sql::escape_ident(child_alias),
604 ),
605 (None, Some(junction)) => {
606 let junction_alias = format!("{}_j", child_alias);
607 format!(
608 "{}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
609 postrust_sql::escape_ident(&self.foreign_schema),
610 postrust_sql::escape_ident(&self.foreign_table),
611 postrust_sql::escape_ident(child_alias),
612 postrust_sql::escape_ident(&junction.schema),
613 postrust_sql::escape_ident(&junction.table),
614 postrust_sql::escape_ident(&junction_alias),
615 junction.child_join(&junction_alias, child_alias),
616 junction.parent_correlation(&junction_alias, parent_alias),
617 )
618 }
619 (None, None) => format!(
620 "{}.{} AS {} WHERE {}",
621 postrust_sql::escape_ident(&self.foreign_schema),
622 postrust_sql::escape_ident(&self.foreign_table),
623 postrust_sql::escape_ident(child_alias),
624 self.correlation(parent_alias, child_alias),
625 ),
626 };
627
628 format!("(SELECT {} FROM {} LIMIT 1)", column_sql, source)
629 }
630
631 pub fn inner_join_predicate(
640 &self,
641 parent_alias: &str,
642 parent_row: &str,
643 child_alias: &str,
644 child_where: Option<&str>,
645 ) -> String {
646 let mut predicate = match &self.function {
647 Some(function) => format!(
648 "EXISTS (SELECT 1 FROM {}.{}({}) AS {} WHERE true",
649 postrust_sql::escape_ident(&function.schema),
650 postrust_sql::escape_ident(&function.name),
651 parent_row,
652 postrust_sql::escape_ident(child_alias),
653 ),
654 None => match &self.junction {
655 Some(junction) => {
656 let junction_alias = format!("{}_j", child_alias);
657 format!(
658 "EXISTS (SELECT 1 FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
659 postrust_sql::escape_ident(&self.foreign_schema),
660 postrust_sql::escape_ident(&self.foreign_table),
661 postrust_sql::escape_ident(child_alias),
662 postrust_sql::escape_ident(&junction.schema),
663 postrust_sql::escape_ident(&junction.table),
664 postrust_sql::escape_ident(&junction_alias),
665 junction.child_join(&junction_alias, child_alias),
666 junction.parent_correlation(&junction_alias, parent_alias),
667 )
668 }
669 None => format!(
670 "EXISTS (SELECT 1 FROM {}.{} AS {} WHERE {}",
671 postrust_sql::escape_ident(&self.foreign_schema),
672 postrust_sql::escape_ident(&self.foreign_table),
673 postrust_sql::escape_ident(child_alias),
674 self.correlation(parent_alias, child_alias),
675 ),
676 },
677 };
678
679 if let Some(child_where) = child_where {
680 predicate.push_str(" AND ");
681 predicate.push_str(child_where);
682 }
683
684 predicate.push(')');
685 predicate
686 }
687
688 fn projection(&self, columns: &[String]) -> String {
694 if columns.is_empty() {
695 return "*".to_string();
696 }
697
698 let mut wanted: Vec<&str> = Vec::with_capacity(columns.len() + 1);
699 for column in columns {
700 if !wanted.contains(&column.as_str()) {
701 wanted.push(column);
702 }
703 }
704 if !wanted.contains(&self.foreign_column.as_str()) {
705 wanted.push(&self.foreign_column);
706 }
707
708 wanted
709 .into_iter()
710 .map(postrust_sql::escape_ident)
711 .collect::<Vec<_>>()
712 .join(", ")
713 }
714}
715
716fn castable_type_name(pg_type: &str) -> Option<&str> {
718 if pg_type.is_empty() {
719 return None;
720 }
721 if pg_type
722 .chars()
723 .all(|c| c.is_ascii_alphanumeric() || c == '_')
724 {
725 Some(pg_type)
726 } else {
727 None
728 }
729}
730
731pub fn key_to_text(value: &serde_json::Value) -> Option<String> {
736 match value {
737 serde_json::Value::Null => None,
738 serde_json::Value::String(s) => Some(s.clone()),
739 other => Some(other.to_string()),
740 }
741}
742
743pub fn group_from_aggregated(
750 rows: Vec<(serde_json::Value, serde_json::Value)>,
751) -> HashMap<String, Vec<serde_json::Value>> {
752 let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::with_capacity(rows.len());
753
754 for (key, children) in rows {
755 let key = key_to_text(&key).unwrap_or_default();
756 let children = match children {
757 serde_json::Value::Array(items) => items,
758 _ => Vec::new(),
760 };
761 grouped.entry(key).or_default().extend(children);
762 }
763
764 grouped
765}
766
767pub fn group_by_key(
768 children: Vec<serde_json::Value>,
769 foreign_column: &str,
770) -> HashMap<String, Vec<serde_json::Value>> {
771 let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
772
773 for child in children {
774 let key = child
775 .get(foreign_column)
776 .and_then(key_to_text)
777 .unwrap_or_default();
778 grouped.entry(key).or_default().push(child);
779 }
780
781 grouped
782}
783
784pub fn attach_to_parent(
790 parent: &mut serde_json::Value,
791 field_name: &str,
792 plan: &EmbedPlan,
793 grouped: &HashMap<String, Vec<serde_json::Value>>,
794) {
795 let key = parent.get(&plan.local_column).and_then(key_to_text);
796
797 let matches = key
798 .as_ref()
799 .and_then(|k| grouped.get(k))
800 .cloned()
801 .unwrap_or_default();
802
803 let value = if plan.is_list {
804 serde_json::Value::Array(matches)
805 } else {
806 matches
807 .into_iter()
808 .next()
809 .unwrap_or(serde_json::Value::Null)
810 };
811
812 if let Some(object) = parent.as_object_mut() {
813 object.insert(field_name.to_string(), value);
814 }
815}
816
817pub fn spread_into_parent(
836 parent: &mut serde_json::Value,
837 plan: &EmbedPlan,
838 grouped: &HashMap<String, Vec<serde_json::Value>>,
839 columns: &[(String, String)],
840) {
841 let key = parent.get(&plan.local_column).and_then(key_to_text);
842 let matches = key
843 .as_ref()
844 .and_then(|k| grouped.get(k))
845 .cloned()
846 .unwrap_or_default();
847
848 let Some(object) = parent.as_object_mut() else {
849 return;
850 };
851
852 for (source, output) in columns {
853 let value = if plan.is_list {
854 serde_json::Value::Array(
855 matches
856 .iter()
857 .map(|child| {
858 child
859 .get(source)
860 .cloned()
861 .unwrap_or(serde_json::Value::Null)
862 })
863 .collect(),
864 )
865 } else {
866 matches
867 .first()
868 .and_then(|child| child.get(source).cloned())
869 .unwrap_or(serde_json::Value::Null)
870 };
871 object.insert(output.clone(), value);
872 }
873}
874
875pub fn parent_keys(parents: &[serde_json::Value], local_column: &str) -> Vec<String> {
877 let mut seen = std::collections::HashSet::new();
878 let mut keys = Vec::new();
879
880 for parent in parents {
881 if let Some(key) = parent.get(local_column).and_then(key_to_text) {
882 if seen.insert(key.clone()) {
883 keys.push(key);
884 }
885 }
886 }
887
888 keys
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894
895 fn plan(is_list: bool) -> EmbedPlan {
896 EmbedPlan {
897 row_argument: None,
898 local_column: "id".into(),
899 foreign_column: "user_id".into(),
900 foreign_column_type: "int4".into(),
901 foreign_schema: "public".into(),
902 foreign_table: "posts".into(),
903 is_list,
904 columns: vec![("id".into(), "user_id".into())],
905 junction: None,
906 function: None,
907 }
908 }
909
910 #[test]
911 fn children_sql_binds_keys_as_a_cast_array() {
912 let sql = plan(true).children_sql(None, &[]).unwrap();
913 assert_eq!(
914 sql,
915 "SELECT row_to_json(t) FROM (SELECT * FROM \"public\".\"posts\" \
916 WHERE \"user_id\" = ANY($1::int4[])) t"
917 );
918 }
919
920 #[test]
921 fn embed_expression_aggregates_a_to_many_relation() {
922 let sql = plan(true)
923 .embed_expression(
924 "p",
925 "\"p\"",
926 "posts",
927 r#""id", "title""#,
928 None,
929 0,
930 None,
931 None,
932 None,
933 )
934 .unwrap();
935
936 assert!(sql.starts_with("COALESCE((SELECT json_agg("), "{}", sql);
937 assert!(sql.contains(r#""posts"."user_id" = "p"."id""#), "{}", sql);
940 assert!(
941 sql.contains(r#"AS "posts""#),
942 "the child table is aliased: {}",
943 sql
944 );
945 assert!(!sql.contains("ANY("), "{}", sql);
946 assert!(sql.contains("'[]'::json"), "{}", sql);
948 }
949
950 #[test]
951 fn embed_expression_takes_one_row_for_a_to_one_relation() {
952 let sql = plan(false)
953 .embed_expression("p", "\"p\"", "author", r#""id""#, None, 0, None, None, None)
954 .unwrap();
955
956 assert!(sql.contains("row_to_json"), "{}", sql);
957 assert!(!sql.contains("json_agg"), "{}", sql);
958 assert!(
959 sql.contains("LIMIT 1"),
960 "a to-one relation yields one row: {}",
961 sql
962 );
963 }
964
965 #[test]
966 fn embed_expression_limits_rows_per_parent() {
967 let sql = plan(true)
968 .embed_expression(
969 "p",
970 "\"p\"",
971 "posts",
972 r#""id""#,
973 Some(25),
974 0,
975 None,
976 None,
977 None,
978 )
979 .unwrap();
980 assert!(sql.contains("LIMIT 25"), "{}", sql);
981 }
982
983 #[test]
984 fn children_sql_projects_only_the_requested_columns() {
985 let sql = plan(true)
986 .children_sql(None, &["title".to_string(), "body".to_string()])
987 .unwrap();
988
989 assert!(
990 sql.contains(r#"SELECT "title", "body", "user_id" FROM"#),
991 "{}",
992 sql
993 );
994 assert!(
995 !sql.contains("SELECT *"),
996 "an unrequested column should not be read at all: {}",
997 sql
998 );
999 }
1000
1001 #[test]
1002 fn children_sql_always_includes_the_join_column() {
1003 let sql = plan(true)
1006 .children_sql(None, &["title".to_string()])
1007 .unwrap();
1008 assert!(sql.contains(r#""user_id""#), "{}", sql);
1009 }
1010
1011 #[test]
1012 fn children_sql_does_not_repeat_the_join_column() {
1013 let sql = plan(true)
1014 .children_sql(None, &["user_id".to_string(), "title".to_string()])
1015 .unwrap();
1016 assert_eq!(
1017 sql.matches(r#""user_id""#).count(),
1018 2,
1019 "expected the column once in the projection and once in the WHERE: {}",
1020 sql
1021 );
1022 }
1023
1024 #[test]
1025 fn children_sql_escapes_column_names() {
1026 let sql = plan(true)
1028 .children_sql(None, &[r#"ev"il"#.to_string()])
1029 .unwrap();
1030 assert!(sql.contains(r#""ev""il""#), "{}", sql);
1031 }
1032
1033 #[test]
1034 fn children_sql_falls_back_to_every_column() {
1035 let sql = plan(true).children_sql(None, &[]).unwrap();
1036 assert!(sql.contains("SELECT * FROM"), "{}", sql);
1037 }
1038
1039 #[test]
1040 fn children_sql_applies_a_limit() {
1041 let sql = plan(true).children_sql(Some(25), &[]).unwrap();
1042 assert!(sql.contains("LIMIT 25"), "{}", sql);
1043 }
1044
1045 #[test]
1046 fn children_sql_rejects_a_non_plain_type_name() {
1047 let mut p = plan(true);
1048 p.foreign_column_type = "int4; DROP TABLE users".into();
1049 assert!(p.children_sql(None, &[]).is_err());
1050 }
1051
1052 #[test]
1053 fn keys_are_rendered_without_json_quoting() {
1054 assert_eq!(key_to_text(&serde_json::json!(7)), Some("7".to_string()));
1055 assert_eq!(
1056 key_to_text(&serde_json::json!("abc")),
1057 Some("abc".to_string())
1058 );
1059 assert_eq!(key_to_text(&serde_json::Value::Null), None);
1060 }
1061
1062 #[test]
1063 fn parent_keys_are_distinct_and_skip_nulls() {
1064 let parents = vec![
1065 serde_json::json!({"id": 1}),
1066 serde_json::json!({"id": 2}),
1067 serde_json::json!({"id": 1}),
1068 serde_json::json!({"id": null}),
1069 ];
1070 assert_eq!(parent_keys(&parents, "id"), vec!["1", "2"]);
1071 }
1072
1073 #[test]
1074 fn to_many_attaches_an_array_and_empty_when_absent() {
1075 let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1076
1077 let mut matched = serde_json::json!({"id": 1});
1078 attach_to_parent(&mut matched, "posts", &plan(true), &grouped);
1079 assert_eq!(matched["posts"].as_array().map(|a| a.len()), Some(1));
1080
1081 let mut unmatched = serde_json::json!({"id": 2});
1082 attach_to_parent(&mut unmatched, "posts", &plan(true), &grouped);
1083 assert_eq!(
1084 unmatched["posts"],
1085 serde_json::json!([]),
1086 "a to-many with no matches must still be an array"
1087 );
1088 }
1089
1090 #[test]
1091 fn to_one_attaches_an_object_or_null() {
1092 let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1093
1094 let mut matched = serde_json::json!({"id": 1});
1095 attach_to_parent(&mut matched, "author", &plan(false), &grouped);
1096 assert_eq!(matched["author"]["id"], serde_json::json!(10));
1097
1098 let mut unmatched = serde_json::json!({"id": 2});
1099 attach_to_parent(&mut unmatched, "author", &plan(false), &grouped);
1100 assert_eq!(unmatched["author"], serde_json::Value::Null);
1101 }
1102
1103 #[test]
1104 fn spread_lifts_a_to_one_relation_into_the_parent() {
1105 let mut grouped = HashMap::new();
1106 grouped.insert(
1107 "1".to_string(),
1108 vec![serde_json::json!({"first_name": "Ada", "last_name": "L"})],
1109 );
1110
1111 let mut parent = serde_json::json!({"id": 1, "title": "Notes"});
1112 spread_into_parent(
1113 &mut parent,
1114 &plan(false),
1115 &grouped,
1116 &[
1117 ("first_name".into(), "first_name".into()),
1118 ("last_name".into(), "last_name".into()),
1119 ],
1120 );
1121
1122 assert_eq!(parent["first_name"], "Ada");
1124 assert_eq!(parent["last_name"], "L");
1125 assert_eq!(parent["title"], "Notes");
1126 assert!(parent.get("posts").is_none());
1127 }
1128
1129 #[test]
1130 fn spread_without_a_match_still_carries_the_requested_keys() {
1131 let mut parent = serde_json::json!({"id": 9});
1132 spread_into_parent(
1133 &mut parent,
1134 &plan(false),
1135 &HashMap::new(),
1136 &[("first_name".into(), "first_name".into())],
1137 );
1138 assert_eq!(parent["first_name"], serde_json::Value::Null);
1139 }
1140
1141 #[test]
1142 fn spread_of_a_to_many_relation_gives_a_column_per_array() {
1143 let mut grouped = HashMap::new();
1144 grouped.insert(
1145 "1".to_string(),
1146 vec![
1147 serde_json::json!({"title": "a"}),
1148 serde_json::json!({"title": "b"}),
1149 ],
1150 );
1151
1152 let mut parent = serde_json::json!({"id": 1});
1153 spread_into_parent(
1154 &mut parent,
1155 &plan(true),
1156 &grouped,
1157 &[("title".into(), "title".into())],
1158 );
1159
1160 assert_eq!(parent["title"], serde_json::json!(["a", "b"]));
1161 }
1162
1163 #[test]
1164 fn spread_without_a_column_list_adds_nothing() {
1165 let mut grouped = HashMap::new();
1168 grouped.insert("1".to_string(), vec![serde_json::json!({"id": 7, "b": 2})]);
1169
1170 let mut parent = serde_json::json!({"id": 1});
1171 spread_into_parent(&mut parent, &plan(false), &grouped, &[]);
1172
1173 assert_eq!(parent, serde_json::json!({"id": 1}));
1174 }
1175}