Skip to main content

toolu_orm_query/select/relational/
config.rs

1//! RelationalSelectBuilder and RelationConfig for eager-loaded relation queries.
2
3/// Configuration for a single relation to be loaded.
4#[derive(Debug, Clone)]
5pub struct RelationConfig {
6  /// The field name in the result struct (e.g. "posts").
7  pub field_name: String,
8  /// The target table to join against.
9  pub target_table: String,
10  /// The column on the source table (e.g., "id").
11  pub local_key: String,
12  /// The column on the target table (e.g., "author_id").
13  pub foreign_key: String,
14  /// The columns to select from the target table.
15  pub target_columns: Vec<String>,
16  /// `true` for has-many, `false` for has-one / belongs-to.
17  pub is_many: bool,
18  /// Nested relations on the target table (reserved).
19  pub nested: Vec<RelationConfig>,
20}
21
22impl RelationConfig {
23  fn new(
24    field_name: &str,
25    target_table: &str,
26    local_key: &str,
27    foreign_key: &str,
28    target_columns: &[&str],
29    is_many: bool,
30  ) -> Self {
31    Self {
32      field_name: field_name.to_owned(),
33      target_table: target_table.to_owned(),
34      local_key: local_key.to_owned(),
35      foreign_key: foreign_key.to_owned(),
36      target_columns: target_columns.iter().map(|c| (*c).to_owned()).collect(),
37      is_many,
38      nested: Vec::new(),
39    }
40  }
41}
42
43/// Builder for relational SELECT queries.
44pub struct RelationalSelectBuilder {
45  pub(super) source_table: String,
46  pub(super) source_columns: Vec<String>,
47  pub(super) relations: Vec<RelationConfig>,
48}
49
50impl RelationalSelectBuilder {
51  /// New builder for the given source table and scalar columns.
52  pub fn new(source_table: &str, columns: &[&str]) -> Self {
53    Self {
54      source_table: source_table.to_owned(),
55      source_columns: columns.iter().map(|c| (*c).to_owned()).collect(),
56      relations: Vec::new(),
57    }
58  }
59
60  /// Add a has-many relation.
61  pub fn with_many(
62    mut self,
63    field_name: &str,
64    target_table: &str,
65    local_key: &str,
66    foreign_key: &str,
67    target_columns: &[&str],
68  ) -> Self {
69    self.relations.push(RelationConfig::new(
70      field_name,
71      target_table,
72      local_key,
73      foreign_key,
74      target_columns,
75      true,
76    ));
77    self
78  }
79
80  /// Add a has-one / belongs-to relation.
81  pub fn with_one(
82    mut self,
83    field_name: &str,
84    target_table: &str,
85    local_key: &str,
86    foreign_key: &str,
87    target_columns: &[&str],
88  ) -> Self {
89    self.relations.push(RelationConfig::new(
90      field_name,
91      target_table,
92      local_key,
93      foreign_key,
94      target_columns,
95      false,
96    ));
97    self
98  }
99
100  /// Relation configurations in declaration order.
101  pub fn relation_configs(&self) -> &[RelationConfig] {
102    &self.relations
103  }
104
105  /// Source table name.
106  pub fn source_table(&self) -> &str {
107    &self.source_table
108  }
109
110  /// Scalar column names on the source table.
111  pub fn source_columns(&self) -> &[String] {
112    &self.source_columns
113  }
114
115  // ── Shared SQL helpers (used by both postgres_sql and sqlite_sql) ──────────
116
117  /// Appends `"table"."col1", "table"."col2", ...` for source columns.
118  pub(super) fn push_source_select(&self, sql: &mut String) {
119    for (i, col) in self.source_columns.iter().enumerate() {
120      if i > 0 {
121        sql.push_str(", ");
122      }
123      push_qualified(sql, &self.source_table, col);
124    }
125  }
126
127  /// Appends `"qualifier"."col1", "qualifier"."col2", ...` for a column list.
128  pub(super) fn push_column_list(sql: &mut String, qualifier: &str, columns: &[String]) {
129    for (i, col) in columns.iter().enumerate() {
130      if i > 0 {
131        sql.push_str(", ");
132      }
133      push_qualified(sql, qualifier, col);
134    }
135  }
136
137  /// Appends `WHERE "target"."fk" = "source"."lk"`.
138  pub(super) fn push_join_where(
139    &self,
140    sql: &mut String,
141    target_qualifier: &str,
142    rel: &RelationConfig,
143  ) {
144    sql.push_str(" WHERE ");
145    push_qualified(sql, target_qualifier, &rel.foreign_key);
146    sql.push_str(" = ");
147    push_qualified(sql, &self.source_table, &rel.local_key);
148    if !rel.is_many {
149      sql.push_str(" LIMIT 1");
150    }
151  }
152}
153
154/// Appends `"qualifier"."column"` to the SQL buffer.
155fn push_qualified(sql: &mut String, qualifier: &str, column: &str) {
156  sql.push('"');
157  sql.push_str(qualifier);
158  sql.push_str("\".\"");
159  sql.push_str(column);
160  sql.push('"');
161}