1use crate::error::{Error, Result};
19use crate::schema_cache::{Relationship, SchemaCache, Table};
20use std::collections::HashMap;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum EmbedRendering {
35 Verbatim,
39 Normalised,
48}
49
50impl EmbedRendering {
51 fn cast(self) -> &'static str {
53 match self {
54 Self::Verbatim => "::json",
55 Self::Normalised => "::jsonb",
56 }
57 }
58}
59
60#[derive(Clone, Debug)]
62pub struct EmbedPlan {
63 pub local_column: String,
65 pub foreign_column: String,
67 pub foreign_column_type: String,
69 pub foreign_schema: String,
71 pub foreign_table: String,
73 pub is_list: bool,
75 pub columns: Vec<(String, String)>,
81 pub junction: Option<EmbedJunction>,
86 pub function: Option<crate::api_request::QualifiedIdentifier>,
93 pub row_argument: Option<String>,
96}
97
98#[derive(Clone, Debug)]
100pub struct EmbedJunction {
101 pub schema: String,
103 pub table: String,
105 pub parent_columns: Vec<(String, String)>,
112 pub child_columns: Vec<(String, String)>,
114}
115
116impl EmbedJunction {
117 fn child_join(&self, junction_alias: &str, child_alias: &str) -> String {
119 Self::equalities(&self.child_columns, junction_alias, child_alias, true)
120 }
121
122 fn parent_correlation(&self, junction_alias: &str, parent_alias: &str) -> String {
124 Self::equalities(&self.parent_columns, junction_alias, parent_alias, false)
125 }
126
127 fn equalities(
133 pairs: &[(String, String)],
134 junction_alias: &str,
135 other_alias: &str,
136 junction_first: bool,
137 ) -> String {
138 pairs
139 .iter()
140 .map(|(left, right)| {
141 let (junction_column, other_column) = match junction_first {
142 true => (left, right),
143 false => (right, left),
144 };
145 format!(
146 "{}.{} = {}.{}",
147 postrust_sql::escape_ident(junction_alias),
148 postrust_sql::escape_ident(junction_column),
149 postrust_sql::escape_ident(other_alias),
150 postrust_sql::escape_ident(other_column),
151 )
152 })
153 .collect::<Vec<_>>()
154 .join(" AND ")
155 }
156}
157
158impl EmbedPlan {
159 pub fn resolve(relationship: &Relationship, schema_cache: &SchemaCache) -> Result<Self> {
164 let foreign_table_qi = relationship.foreign_table().clone();
165
166 if let Relationship::Computed {
169 function,
170 foreign_table,
171 to_one,
172 row_argument,
173 ..
174 } = relationship
175 {
176 return Ok(Self {
177 local_column: String::new(),
178 foreign_column: String::new(),
179 foreign_column_type: String::new(),
180 foreign_schema: foreign_table.schema.clone(),
181 foreign_table: foreign_table.name.clone(),
182 is_list: !to_one,
183 columns: Vec::new(),
184 junction: None,
185 function: Some(function.clone()),
186 row_argument: row_argument.clone(),
187 });
188 }
189
190 if let Relationship::ForeignKey {
191 cardinality: crate::schema_cache::Cardinality::M2M(junction),
192 ..
193 } = relationship
194 {
195 let (local_column, _) =
200 junction.source_columns.first().cloned().ok_or_else(|| {
201 Error::EmbeddingError("junction has no source columns".into())
202 })?;
203 let (_, foreign_column) =
204 junction.target_columns.first().cloned().ok_or_else(|| {
205 Error::EmbeddingError("junction has no target columns".into())
206 })?;
207
208 return Ok(Self {
209 local_column,
210 foreign_column,
211 foreign_column_type: String::new(),
212 foreign_schema: foreign_table_qi.schema.clone(),
213 foreign_table: foreign_table_qi.name.clone(),
214 is_list: true,
216 columns: Vec::new(),
217 row_argument: None,
218 junction: Some(EmbedJunction {
219 schema: junction.table.schema.clone(),
220 table: junction.table.name.clone(),
221 parent_columns: junction.source_columns.clone(),
222 child_columns: junction.target_columns.clone(),
223 }),
224 function: None,
225 });
226 }
227
228 let columns = match relationship {
229 Relationship::ForeignKey { cardinality, .. } => cardinality.columns(),
230 Relationship::Computed { .. } => unreachable!("handled above"),
231 };
232
233 let (local_column, foreign_column) = columns
234 .first()
235 .cloned()
236 .ok_or_else(|| Error::EmbeddingError("relationship joins on no columns".into()))?;
237
238 let foreign_table: &Table = schema_cache.get_table(&foreign_table_qi).ok_or_else(|| {
239 Error::EmbeddingError(format!(
240 "cannot embed \"{}\": it is not in an exposed schema",
241 foreign_table_qi
242 ))
243 })?;
244
245 let foreign_column_type = foreign_table
246 .get_column(&foreign_column)
247 .map(|c| c.nominal_type.clone())
248 .ok_or_else(|| {
249 Error::EmbeddingError(format!(
250 "cannot embed \"{}\": join column \"{}\" not found",
251 foreign_table_qi, foreign_column
252 ))
253 })?;
254
255 Ok(Self {
256 local_column,
257 foreign_column,
258 foreign_column_type,
259 foreign_schema: foreign_table_qi.schema.clone(),
260 foreign_table: foreign_table_qi.name.clone(),
261 is_list: !relationship.is_to_one(),
262 columns,
263 junction: None,
264 function: None,
265 row_argument: None,
266 })
267 }
268
269 fn correlation(&self, parent_alias: &str, child_alias: &str) -> String {
274 let pairs = if self.columns.is_empty() {
275 vec![(self.local_column.clone(), self.foreign_column.clone())]
276 } else {
277 self.columns.clone()
278 };
279
280 pairs
281 .iter()
282 .map(|(local, foreign)| {
283 format!(
284 "{}.{} = {}.{}",
285 postrust_sql::escape_ident(child_alias),
286 postrust_sql::escape_ident(foreign),
287 postrust_sql::escape_ident(parent_alias),
288 postrust_sql::escape_ident(local),
289 )
290 })
291 .collect::<Vec<_>>()
292 .join(" AND ")
293 }
294
295 pub fn children_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
309 let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
310 Error::EmbeddingError(format!(
311 "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
312 self.foreign_table, self.foreign_column_type
313 ))
314 })?;
315
316 let projection = self.projection(columns);
317
318 let mut inner = format!(
319 "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
320 projection,
321 postrust_sql::escape_ident(&self.foreign_schema),
322 postrust_sql::escape_ident(&self.foreign_table),
323 postrust_sql::escape_ident(&self.foreign_column),
324 type_name
325 );
326
327 if let Some(limit) = limit {
328 inner.push_str(&format!(" LIMIT {}", limit));
329 }
330
331 Ok(format!("SELECT row_to_json(t) FROM ({}) t", inner))
332 }
333
334 pub fn children_grouped_sql(&self, limit: Option<i64>, columns: &[String]) -> Result<String> {
349 if self
350 .junction
351 .as_ref()
352 .is_some_and(|j| j.parent_columns.len() > 1)
353 {
354 return Err(Error::EmbeddingError(format!(
355 "embedding \"{}\" this way is not supported: it joins through a junction \
356 keyed on several columns, and grouping children by key needs a single one",
357 self.foreign_table,
358 )));
359 }
360
361 if self.columns.len() > 1 {
362 return Err(Error::EmbeddingError(format!(
363 "embedding \"{}\" this way is not supported: it joins on {} columns, and \
364 grouping children by key needs a single one",
365 self.foreign_table,
366 self.columns.len()
367 )));
368 }
369
370 let type_name = castable_type_name(&self.foreign_column_type).ok_or_else(|| {
371 Error::EmbeddingError(format!(
372 "cannot embed \"{}\": join column type \"{}\" is not a plain type name",
373 self.foreign_table, self.foreign_column_type
374 ))
375 })?;
376
377 let key = postrust_sql::escape_ident(&self.foreign_column);
378
379 let mut inner = format!(
380 "SELECT {} FROM {}.{} WHERE {} = ANY($1::{}[])",
381 self.projection(columns),
382 postrust_sql::escape_ident(&self.foreign_schema),
383 postrust_sql::escape_ident(&self.foreign_table),
384 key,
385 type_name
386 );
387
388 if let Some(limit) = limit {
389 inner.push_str(&format!(" LIMIT {}", limit));
390 }
391
392 Ok(format!(
393 "SELECT to_jsonb(c.{key}) AS k, json_agg(row_to_json(c)) AS v \
394 FROM ({inner}) c GROUP BY c.{key}",
395 key = key,
396 inner = inner
397 ))
398 }
399
400 #[allow(clippy::too_many_arguments)] pub fn correlated_rows(
417 &self,
418 parent_alias: &str,
419 parent_row: &str,
420 child_alias: &str,
421 inner_select: &str,
422 limit: Option<i64>,
423 offset: i64,
427 child_where: Option<&str>,
428 order_by: Option<&str>,
432 function_arguments: Option<&str>,
435 ) -> Result<String> {
436 let mut inner = match &self.function {
444 Some(function) => format!(
445 "SELECT {} FROM {}.{}({}) AS {} WHERE true",
446 inner_select,
447 postrust_sql::escape_ident(&function.schema),
448 postrust_sql::escape_ident(&function.name),
449 function_arguments.unwrap_or(parent_row),
454 postrust_sql::escape_ident(child_alias),
455 ),
456 None => match &self.junction {
457 Some(junction) => {
460 let junction_alias = format!("{}_j", child_alias);
461 format!(
462 "SELECT {} FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
463 inner_select,
464 postrust_sql::escape_ident(&self.foreign_schema),
465 postrust_sql::escape_ident(&self.foreign_table),
466 postrust_sql::escape_ident(child_alias),
467 postrust_sql::escape_ident(&junction.schema),
468 postrust_sql::escape_ident(&junction.table),
469 postrust_sql::escape_ident(&junction_alias),
470 junction.child_join(&junction_alias, child_alias),
471 junction.parent_correlation(&junction_alias, parent_alias),
472 )
473 }
474 None => format!(
475 "SELECT {} FROM {}.{} AS {} WHERE {}",
476 inner_select,
477 postrust_sql::escape_ident(&self.foreign_schema),
478 postrust_sql::escape_ident(&self.foreign_table),
479 postrust_sql::escape_ident(child_alias),
480 self.correlation(parent_alias, child_alias),
481 ),
482 },
483 };
484
485 if let Some(child_where) = child_where {
489 inner.push_str(" AND ");
490 inner.push_str(child_where);
491 }
492
493 if let Some(order_by) = order_by {
494 inner.push_str(" ORDER BY ");
495 inner.push_str(order_by);
496 }
497
498 if let Some(limit) = limit {
503 inner.push_str(&format!(" LIMIT {}", limit));
504 } else if !self.is_list {
505 inner.push_str(" LIMIT 1");
506 }
507
508 if offset > 0 {
509 inner.push_str(&format!(" OFFSET {}", offset));
510 }
511
512 Ok(inner)
513 }
514
515 #[allow(clippy::too_many_arguments)] pub fn embed_expression(
522 &self,
523 parent_alias: &str,
524 parent_row: &str,
525 child_alias: &str,
526 inner_select: &str,
527 limit: Option<i64>,
528 offset: i64,
529 child_where: Option<&str>,
530 order_by: Option<&str>,
531 function_arguments: Option<&str>,
532 rendering: EmbedRendering,
533 ) -> Result<String> {
534 let inner = self.correlated_rows(
535 parent_alias,
536 parent_row,
537 child_alias,
538 inner_select,
539 limit,
540 offset,
541 child_where,
542 order_by,
543 function_arguments,
544 )?;
545
546 let alias = postrust_sql::escape_ident(&format!("{}_j", child_alias));
547
548 Ok(if self.is_list {
549 format!(
552 "COALESCE((SELECT json_agg(row_to_json({alias})) FROM ({inner}) {alias}), '[]'::json){cast}",
553 alias = alias,
554 inner = inner,
555 cast = rendering.cast()
556 )
557 } else {
558 format!(
559 "(SELECT row_to_json({alias}) FROM ({inner}) {alias}){cast}",
560 alias = alias,
561 inner = inner,
562 cast = rendering.cast()
563 )
564 })
565 }
566
567 #[allow(clippy::too_many_arguments)] pub fn aggregate_expression(
583 &self,
584 parent_alias: &str,
585 parent_row: &str,
586 child_alias: &str,
587 aggregate_select: &str,
588 limit: Option<i64>,
589 offset: i64,
590 child_where: Option<&str>,
591 order_by: Option<&str>,
592 function_arguments: Option<&str>,
593 distinct_on: &str,
597 rendering: EmbedRendering,
598 ) -> Result<String> {
599 let child = postrust_sql::escape_ident(child_alias);
600 let rows = self.correlated_rows(
601 parent_alias,
602 parent_row,
603 child_alias,
604 &format!("{}{}.*", distinct_on, child),
605 limit,
606 offset,
607 child_where,
608 order_by,
609 function_arguments,
610 )?;
611 let alias = postrust_sql::escape_ident(&format!("{}_a", child_alias));
612 Ok(format!(
613 "(SELECT row_to_json({alias}) FROM \
614 (SELECT {select} FROM ({rows}) AS {child}) {alias}){cast}",
615 alias = alias,
616 select = aggregate_select,
617 rows = rows,
618 child = child,
619 cast = rendering.cast()
620 ))
621 }
622
623 pub fn order_expression(
631 &self,
632 parent_alias: &str,
633 parent_row: &str,
634 child_alias: &str,
635 column_sql: &str,
636 ) -> String {
637 let source = match (&self.function, &self.junction) {
638 (Some(function), _) => format!(
639 "{}.{}({}) AS {} WHERE true",
640 postrust_sql::escape_ident(&function.schema),
641 postrust_sql::escape_ident(&function.name),
642 parent_row,
643 postrust_sql::escape_ident(child_alias),
644 ),
645 (None, Some(junction)) => {
646 let junction_alias = format!("{}_j", child_alias);
647 format!(
648 "{}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
649 postrust_sql::escape_ident(&self.foreign_schema),
650 postrust_sql::escape_ident(&self.foreign_table),
651 postrust_sql::escape_ident(child_alias),
652 postrust_sql::escape_ident(&junction.schema),
653 postrust_sql::escape_ident(&junction.table),
654 postrust_sql::escape_ident(&junction_alias),
655 junction.child_join(&junction_alias, child_alias),
656 junction.parent_correlation(&junction_alias, parent_alias),
657 )
658 }
659 (None, None) => format!(
660 "{}.{} AS {} WHERE {}",
661 postrust_sql::escape_ident(&self.foreign_schema),
662 postrust_sql::escape_ident(&self.foreign_table),
663 postrust_sql::escape_ident(child_alias),
664 self.correlation(parent_alias, child_alias),
665 ),
666 };
667
668 format!("(SELECT {} FROM {} LIMIT 1)", column_sql, source)
669 }
670
671 pub fn inner_join_predicate(
680 &self,
681 parent_alias: &str,
682 parent_row: &str,
683 child_alias: &str,
684 child_where: Option<&str>,
685 ) -> String {
686 let mut predicate = match &self.function {
687 Some(function) => format!(
688 "EXISTS (SELECT 1 FROM {}.{}({}) AS {} WHERE true",
689 postrust_sql::escape_ident(&function.schema),
690 postrust_sql::escape_ident(&function.name),
691 parent_row,
692 postrust_sql::escape_ident(child_alias),
693 ),
694 None => match &self.junction {
695 Some(junction) => {
696 let junction_alias = format!("{}_j", child_alias);
697 format!(
698 "EXISTS (SELECT 1 FROM {}.{} AS {} JOIN {}.{} AS {} ON {} WHERE {}",
699 postrust_sql::escape_ident(&self.foreign_schema),
700 postrust_sql::escape_ident(&self.foreign_table),
701 postrust_sql::escape_ident(child_alias),
702 postrust_sql::escape_ident(&junction.schema),
703 postrust_sql::escape_ident(&junction.table),
704 postrust_sql::escape_ident(&junction_alias),
705 junction.child_join(&junction_alias, child_alias),
706 junction.parent_correlation(&junction_alias, parent_alias),
707 )
708 }
709 None => format!(
710 "EXISTS (SELECT 1 FROM {}.{} AS {} WHERE {}",
711 postrust_sql::escape_ident(&self.foreign_schema),
712 postrust_sql::escape_ident(&self.foreign_table),
713 postrust_sql::escape_ident(child_alias),
714 self.correlation(parent_alias, child_alias),
715 ),
716 },
717 };
718
719 if let Some(child_where) = child_where {
720 predicate.push_str(" AND ");
721 predicate.push_str(child_where);
722 }
723
724 predicate.push(')');
725 predicate
726 }
727
728 fn projection(&self, columns: &[String]) -> String {
734 if columns.is_empty() {
735 return "*".to_string();
736 }
737
738 let mut wanted: Vec<&str> = Vec::with_capacity(columns.len() + 1);
739 for column in columns {
740 if !wanted.contains(&column.as_str()) {
741 wanted.push(column);
742 }
743 }
744 if !wanted.contains(&self.foreign_column.as_str()) {
745 wanted.push(&self.foreign_column);
746 }
747
748 wanted
749 .into_iter()
750 .map(postrust_sql::escape_ident)
751 .collect::<Vec<_>>()
752 .join(", ")
753 }
754}
755
756fn castable_type_name(pg_type: &str) -> Option<&str> {
758 if pg_type.is_empty() {
759 return None;
760 }
761 if pg_type
762 .chars()
763 .all(|c| c.is_ascii_alphanumeric() || c == '_')
764 {
765 Some(pg_type)
766 } else {
767 None
768 }
769}
770
771pub fn key_to_text(value: &serde_json::Value) -> Option<String> {
776 match value {
777 serde_json::Value::Null => None,
778 serde_json::Value::String(s) => Some(s.clone()),
779 other => Some(other.to_string()),
780 }
781}
782
783pub fn group_from_aggregated(
790 rows: Vec<(serde_json::Value, serde_json::Value)>,
791) -> HashMap<String, Vec<serde_json::Value>> {
792 let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::with_capacity(rows.len());
793
794 for (key, children) in rows {
795 let key = key_to_text(&key).unwrap_or_default();
796 let children = match children {
797 serde_json::Value::Array(items) => items,
798 _ => Vec::new(),
800 };
801 grouped.entry(key).or_default().extend(children);
802 }
803
804 grouped
805}
806
807pub fn group_by_key(
813 children: Vec<serde_json::Value>,
814 foreign_column: &str,
815) -> HashMap<String, Vec<serde_json::Value>> {
816 let mut grouped: HashMap<String, Vec<serde_json::Value>> = HashMap::new();
817
818 for child in children {
819 let key = child
820 .get(foreign_column)
821 .and_then(key_to_text)
822 .unwrap_or_default();
823 grouped.entry(key).or_default().push(child);
824 }
825
826 grouped
827}
828
829pub fn attach_to_parent(
835 parent: &mut serde_json::Value,
836 field_name: &str,
837 plan: &EmbedPlan,
838 grouped: &HashMap<String, Vec<serde_json::Value>>,
839) {
840 let key = parent.get(&plan.local_column).and_then(key_to_text);
841
842 let matches = key
843 .as_ref()
844 .and_then(|k| grouped.get(k))
845 .cloned()
846 .unwrap_or_default();
847
848 let value = if plan.is_list {
849 serde_json::Value::Array(matches)
850 } else {
851 matches
852 .into_iter()
853 .next()
854 .unwrap_or(serde_json::Value::Null)
855 };
856
857 if let Some(object) = parent.as_object_mut() {
858 object.insert(field_name.to_string(), value);
859 }
860}
861
862pub fn spread_into_parent(
881 parent: &mut serde_json::Value,
882 plan: &EmbedPlan,
883 grouped: &HashMap<String, Vec<serde_json::Value>>,
884 columns: &[(String, String)],
885) {
886 let key = parent.get(&plan.local_column).and_then(key_to_text);
887 let matches = key
888 .as_ref()
889 .and_then(|k| grouped.get(k))
890 .cloned()
891 .unwrap_or_default();
892
893 let Some(object) = parent.as_object_mut() else {
894 return;
895 };
896
897 for (source, output) in columns {
898 let value = if plan.is_list {
899 serde_json::Value::Array(
900 matches
901 .iter()
902 .map(|child| {
903 child
904 .get(source)
905 .cloned()
906 .unwrap_or(serde_json::Value::Null)
907 })
908 .collect(),
909 )
910 } else {
911 matches
912 .first()
913 .and_then(|child| child.get(source).cloned())
914 .unwrap_or(serde_json::Value::Null)
915 };
916 object.insert(output.clone(), value);
917 }
918}
919
920pub fn parent_keys(parents: &[serde_json::Value], local_column: &str) -> Vec<String> {
922 let mut seen = std::collections::HashSet::new();
923 let mut keys = Vec::new();
924
925 for parent in parents {
926 if let Some(key) = parent.get(local_column).and_then(key_to_text) {
927 if seen.insert(key.clone()) {
928 keys.push(key);
929 }
930 }
931 }
932
933 keys
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 fn plan(is_list: bool) -> EmbedPlan {
941 EmbedPlan {
942 row_argument: None,
943 local_column: "id".into(),
944 foreign_column: "user_id".into(),
945 foreign_column_type: "int4".into(),
946 foreign_schema: "public".into(),
947 foreign_table: "posts".into(),
948 is_list,
949 columns: vec![("id".into(), "user_id".into())],
950 junction: None,
951 function: None,
952 }
953 }
954
955 #[test]
956 fn children_sql_binds_keys_as_a_cast_array() {
957 let sql = plan(true).children_sql(None, &[]).unwrap();
958 assert_eq!(
959 sql,
960 "SELECT row_to_json(t) FROM (SELECT * FROM \"public\".\"posts\" \
961 WHERE \"user_id\" = ANY($1::int4[])) t"
962 );
963 }
964
965 #[test]
966 fn embed_expression_aggregates_a_to_many_relation() {
967 let sql = plan(true)
968 .embed_expression(
969 "p",
970 "\"p\"",
971 "posts",
972 r#""id", "title""#,
973 None,
974 0,
975 None,
976 None,
977 None,
978 EmbedRendering::Verbatim,
979 )
980 .unwrap();
981
982 assert!(sql.starts_with("COALESCE((SELECT json_agg("), "{}", sql);
983 assert!(sql.contains(r#""posts"."user_id" = "p"."id""#), "{}", sql);
986 assert!(
987 sql.contains(r#"AS "posts""#),
988 "the child table is aliased: {}",
989 sql
990 );
991 assert!(!sql.contains("ANY("), "{}", sql);
992 assert!(sql.contains("'[]'::json"), "{}", sql);
994 }
995
996 #[test]
997 fn embed_expression_takes_one_row_for_a_to_one_relation() {
998 let sql = plan(false)
999 .embed_expression(
1000 "p",
1001 "\"p\"",
1002 "author",
1003 r#""id""#,
1004 None,
1005 0,
1006 None,
1007 None,
1008 None,
1009 EmbedRendering::Verbatim,
1010 )
1011 .unwrap();
1012
1013 assert!(sql.contains("row_to_json"), "{}", sql);
1014 assert!(!sql.contains("json_agg"), "{}", sql);
1015 assert!(
1016 sql.contains("LIMIT 1"),
1017 "a to-one relation yields one row: {}",
1018 sql
1019 );
1020 }
1021
1022 #[test]
1023 fn embed_expression_limits_rows_per_parent() {
1024 let sql = plan(true)
1025 .embed_expression(
1026 "p",
1027 "\"p\"",
1028 "posts",
1029 r#""id""#,
1030 Some(25),
1031 0,
1032 None,
1033 None,
1034 None,
1035 EmbedRendering::Verbatim,
1036 )
1037 .unwrap();
1038 assert!(sql.contains("LIMIT 25"), "{}", sql);
1039 }
1040
1041 #[test]
1042 fn children_sql_projects_only_the_requested_columns() {
1043 let sql = plan(true)
1044 .children_sql(None, &["title".to_string(), "body".to_string()])
1045 .unwrap();
1046
1047 assert!(
1048 sql.contains(r#"SELECT "title", "body", "user_id" FROM"#),
1049 "{}",
1050 sql
1051 );
1052 assert!(
1053 !sql.contains("SELECT *"),
1054 "an unrequested column should not be read at all: {}",
1055 sql
1056 );
1057 }
1058
1059 #[test]
1060 fn children_sql_always_includes_the_join_column() {
1061 let sql = plan(true)
1064 .children_sql(None, &["title".to_string()])
1065 .unwrap();
1066 assert!(sql.contains(r#""user_id""#), "{}", sql);
1067 }
1068
1069 #[test]
1070 fn children_sql_does_not_repeat_the_join_column() {
1071 let sql = plan(true)
1072 .children_sql(None, &["user_id".to_string(), "title".to_string()])
1073 .unwrap();
1074 assert_eq!(
1075 sql.matches(r#""user_id""#).count(),
1076 2,
1077 "expected the column once in the projection and once in the WHERE: {}",
1078 sql
1079 );
1080 }
1081
1082 #[test]
1083 fn children_sql_escapes_column_names() {
1084 let sql = plan(true)
1086 .children_sql(None, &[r#"ev"il"#.to_string()])
1087 .unwrap();
1088 assert!(sql.contains(r#""ev""il""#), "{}", sql);
1089 }
1090
1091 #[test]
1092 fn children_sql_falls_back_to_every_column() {
1093 let sql = plan(true).children_sql(None, &[]).unwrap();
1094 assert!(sql.contains("SELECT * FROM"), "{}", sql);
1095 }
1096
1097 #[test]
1098 fn children_sql_applies_a_limit() {
1099 let sql = plan(true).children_sql(Some(25), &[]).unwrap();
1100 assert!(sql.contains("LIMIT 25"), "{}", sql);
1101 }
1102
1103 #[test]
1104 fn children_sql_rejects_a_non_plain_type_name() {
1105 let mut p = plan(true);
1106 p.foreign_column_type = "int4; DROP TABLE users".into();
1107 assert!(p.children_sql(None, &[]).is_err());
1108 }
1109
1110 #[test]
1111 fn keys_are_rendered_without_json_quoting() {
1112 assert_eq!(key_to_text(&serde_json::json!(7)), Some("7".to_string()));
1113 assert_eq!(
1114 key_to_text(&serde_json::json!("abc")),
1115 Some("abc".to_string())
1116 );
1117 assert_eq!(key_to_text(&serde_json::Value::Null), None);
1118 }
1119
1120 #[test]
1121 fn parent_keys_are_distinct_and_skip_nulls() {
1122 let parents = vec![
1123 serde_json::json!({"id": 1}),
1124 serde_json::json!({"id": 2}),
1125 serde_json::json!({"id": 1}),
1126 serde_json::json!({"id": null}),
1127 ];
1128 assert_eq!(parent_keys(&parents, "id"), vec!["1", "2"]);
1129 }
1130
1131 #[test]
1132 fn to_many_attaches_an_array_and_empty_when_absent() {
1133 let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1134
1135 let mut matched = serde_json::json!({"id": 1});
1136 attach_to_parent(&mut matched, "posts", &plan(true), &grouped);
1137 assert_eq!(matched["posts"].as_array().map(|a| a.len()), Some(1));
1138
1139 let mut unmatched = serde_json::json!({"id": 2});
1140 attach_to_parent(&mut unmatched, "posts", &plan(true), &grouped);
1141 assert_eq!(
1142 unmatched["posts"],
1143 serde_json::json!([]),
1144 "a to-many with no matches must still be an array"
1145 );
1146 }
1147
1148 #[test]
1149 fn to_one_attaches_an_object_or_null() {
1150 let grouped = group_by_key(vec![serde_json::json!({"id": 10, "user_id": 1})], "user_id");
1151
1152 let mut matched = serde_json::json!({"id": 1});
1153 attach_to_parent(&mut matched, "author", &plan(false), &grouped);
1154 assert_eq!(matched["author"]["id"], serde_json::json!(10));
1155
1156 let mut unmatched = serde_json::json!({"id": 2});
1157 attach_to_parent(&mut unmatched, "author", &plan(false), &grouped);
1158 assert_eq!(unmatched["author"], serde_json::Value::Null);
1159 }
1160
1161 #[test]
1162 fn spread_lifts_a_to_one_relation_into_the_parent() {
1163 let mut grouped = HashMap::new();
1164 grouped.insert(
1165 "1".to_string(),
1166 vec![serde_json::json!({"first_name": "Ada", "last_name": "L"})],
1167 );
1168
1169 let mut parent = serde_json::json!({"id": 1, "title": "Notes"});
1170 spread_into_parent(
1171 &mut parent,
1172 &plan(false),
1173 &grouped,
1174 &[
1175 ("first_name".into(), "first_name".into()),
1176 ("last_name".into(), "last_name".into()),
1177 ],
1178 );
1179
1180 assert_eq!(parent["first_name"], "Ada");
1182 assert_eq!(parent["last_name"], "L");
1183 assert_eq!(parent["title"], "Notes");
1184 assert!(parent.get("posts").is_none());
1185 }
1186
1187 #[test]
1188 fn spread_without_a_match_still_carries_the_requested_keys() {
1189 let mut parent = serde_json::json!({"id": 9});
1190 spread_into_parent(
1191 &mut parent,
1192 &plan(false),
1193 &HashMap::new(),
1194 &[("first_name".into(), "first_name".into())],
1195 );
1196 assert_eq!(parent["first_name"], serde_json::Value::Null);
1197 }
1198
1199 #[test]
1200 fn spread_of_a_to_many_relation_gives_a_column_per_array() {
1201 let mut grouped = HashMap::new();
1202 grouped.insert(
1203 "1".to_string(),
1204 vec![
1205 serde_json::json!({"title": "a"}),
1206 serde_json::json!({"title": "b"}),
1207 ],
1208 );
1209
1210 let mut parent = serde_json::json!({"id": 1});
1211 spread_into_parent(
1212 &mut parent,
1213 &plan(true),
1214 &grouped,
1215 &[("title".into(), "title".into())],
1216 );
1217
1218 assert_eq!(parent["title"], serde_json::json!(["a", "b"]));
1219 }
1220
1221 #[test]
1222 fn spread_without_a_column_list_adds_nothing() {
1223 let mut grouped = HashMap::new();
1226 grouped.insert("1".to_string(), vec![serde_json::json!({"id": 7, "b": 2})]);
1227
1228 let mut parent = serde_json::json!({"id": 1});
1229 spread_into_parent(&mut parent, &plan(false), &grouped, &[]);
1230
1231 assert_eq!(parent, serde_json::json!({"id": 1}));
1232 }
1233}