scythe_codegen/backend_trait.rs
1use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo};
2use scythe_core::errors::{ErrorCode, ScytheError};
3
4use crate::GeneratedCode;
5
6/// Information needed to generate an RBS type signature file.
7#[derive(Debug, Clone)]
8pub struct RbsGenerationContext {
9 pub queries: Vec<RbsQueryInfo>,
10 pub enums: Vec<RbsEnumInfo>,
11}
12
13/// Per-query info for RBS generation.
14#[derive(Debug, Clone)]
15pub struct RbsQueryInfo {
16 pub func_name: String,
17 pub struct_name: Option<String>,
18 pub columns: Vec<ResolvedColumn>,
19 pub params: Vec<ResolvedParam>,
20 pub command: scythe_core::parser::QueryCommand,
21}
22
23/// Per-enum info for RBS generation.
24#[derive(Debug, Clone)]
25pub struct RbsEnumInfo {
26 pub type_name: String,
27 pub values: Vec<String>,
28}
29
30/// A column with its type resolved to the target language.
31#[derive(Debug, Clone, Default)]
32pub struct ResolvedColumn {
33 pub name: String,
34 pub field_name: String,
35 pub lang_type: String,
36 pub full_type: String,
37 pub neutral_type: String,
38 pub nullable: bool,
39 /// Alias of the outer-joined relation this column came from, when the
40 /// column was widened to nullable by that join. See
41 /// [`scythe_core::analyzer::AnalyzedColumn::join_group`].
42 pub join_group: Option<String>,
43 /// Whether the column was nullable in the schema, before outer-join
44 /// widening.
45 pub nullable_before_join: bool,
46 /// The raw SQL type this column was derived from (see
47 /// [`scythe_core::analyzer::AnalyzedColumn::sql_type`]). Backends that need
48 /// to distinguish storage representations the neutral type collapses
49 /// (Oracle CLOB vs. VARCHAR2, both `neutral_type == "string"`) match on
50 /// this instead of `neutral_type`.
51 pub sql_type: String,
52}
53
54impl ResolvedColumn {
55 /// Whether this column can only be null because its outer join found no
56 /// row — making it a usable discriminant for a union.
57 pub fn is_join_discriminant(&self) -> bool {
58 self.join_group.is_some() && !self.nullable_before_join
59 }
60}
61
62/// A parameter with its type resolved to the target language.
63#[derive(Debug, Clone)]
64pub struct ResolvedParam {
65 pub name: String,
66 pub field_name: String,
67 pub lang_type: String,
68 pub full_type: String,
69 pub borrowed_type: String,
70 pub neutral_type: String,
71 pub nullable: bool,
72}
73
74/// Inputs for [`CodegenBackend::generate_grouped_query_fn`].
75///
76/// The grouped query-fn contract carries enough context (analyzed query, both
77/// struct names, the flat and split column sets, params, and the grouping key)
78/// that passing it positionally trips `clippy::too_many_arguments`. Bundling it
79/// into one struct keeps the per-language implementations uniform as backends
80/// opt in to grouped codegen.
81pub struct GroupedQueryFn<'a> {
82 /// Full analyzed query (SQL, name, params, optional_params, deprecated, …).
83 pub analyzed: &'a AnalyzedQuery,
84 /// Name of the generated parent struct.
85 pub parent_struct_name: &'a str,
86 /// Name of the generated child struct.
87 pub child_struct_name: &'a str,
88 /// All resolved columns in flat SELECT order; used for row decoding.
89 pub all_columns: &'a [ResolvedColumn],
90 /// Resolved columns belonging to the parent struct.
91 pub parent_columns: &'a [ResolvedColumn],
92 /// Resolved columns belonging to the child struct(s).
93 pub child_columns: &'a [ResolvedColumn],
94 /// Resolved query parameters.
95 pub params: &'a [ResolvedParam],
96 /// Grouping key column name in the flat result row
97 /// (matches [`scythe_core::analyzer::GroupByConfig::key_column`]).
98 pub key_column: &'a str,
99}
100
101/// Trait that all codegen backends must implement.
102pub trait CodegenBackend: Send + Sync {
103 /// The backend's name (e.g. "rust-sqlx", "rust-tokio-postgres").
104 fn name(&self) -> &str;
105
106 /// The backend's manifest (type mappings, naming conventions, etc).
107 fn manifest(&self) -> &scythe_backend::manifest::BackendManifest;
108
109 /// Generate a row struct for a query result.
110 fn generate_row_struct(&self, query_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError>;
111
112 /// Generate a model struct for a table.
113 fn generate_model_struct(&self, table_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError>;
114
115 /// Generate a query function.
116 fn generate_query_fn(
117 &self,
118 analyzed: &AnalyzedQuery,
119 struct_name: &str,
120 columns: &[ResolvedColumn],
121 params: &[ResolvedParam],
122 ) -> Result<String, ScytheError>;
123
124 /// Generate an enum definition.
125 fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError>;
126
127 /// Generate a composite type definition.
128 fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError>;
129
130 /// Generate a file-level header (imports, docstring, etc).
131 /// Returns an empty string by default; backends may override.
132 fn file_header(&self) -> String {
133 String::new()
134 }
135
136 /// Generate a file-level header using already generated code as context.
137 ///
138 /// Backends with conditional imports can inspect generated fragments and avoid
139 /// broad engine-level import guesses. The default preserves existing behavior.
140 fn file_header_for_results(&self, _generated: &[GeneratedCode]) -> String {
141 self.file_header()
142 }
143
144 /// Generate a file-level footer (closing braces, etc).
145 /// Returns an empty string by default; backends may override.
146 fn file_footer(&self) -> String {
147 String::new()
148 }
149
150 /// Generate a class header that wraps query functions only.
151 /// When non-empty, the assembly will emit all type definitions (enums,
152 /// row structs, model structs) first, then this class header, then all
153 /// query functions, then the file footer.
154 /// Returns an empty string by default (no class wrapper).
155 fn query_class_header(&self) -> String {
156 String::new()
157 }
158
159 /// Generate code that should be emitted after the file footer.
160 /// This is useful for backends that need top-level code after a class wrapper.
161 /// For example, C# extension methods must be top-level, not nested.
162 /// Returns an empty string by default.
163 fn post_footer(&self) -> String {
164 String::new()
165 }
166
167 /// Generate an RBS type signature file for Ruby backends.
168 /// Returns `None` by default; Ruby backends override this.
169 fn generate_rbs_file(&self, _context: &RbsGenerationContext) -> Option<String> {
170 None
171 }
172
173 /// Generate parent and child structs for a `:grouped` query.
174 ///
175 /// A `:grouped` query folds flat rows from a normal SQL SELECT into a nested
176 /// parent/child structure entirely on the client side — the SQL itself is
177 /// unchanged from a regular `:many` query.
178 ///
179 /// ## Struct layout
180 ///
181 /// * **Child struct** (`child_struct_name`): contains all `child_columns`.
182 /// Defined first in the output to avoid forward references.
183 /// * **Parent struct** (`parent_struct_name`): contains all `parent_columns`
184 /// plus one extra field `children: Vec<child_struct_name>` (or the
185 /// language-native equivalent collection type).
186 ///
187 /// ## Grouping semantics
188 ///
189 /// The generated query function fetches flat rows and folds them into an
190 /// **order-preserving** list of parent structs, appending each row's child
191 /// fields to the matching parent's collection. Equality on `key_column` is
192 /// the fold predicate.
193 ///
194 /// ## Parameters
195 ///
196 /// * `parent_struct_name` – fully qualified struct/class name for the parent
197 /// (e.g. `"GetUsersWithOrdersRow"`).
198 /// * `child_struct_name` – fully qualified struct/class name for the child
199 /// (e.g. `"GetUsersWithOrdersChildRow"`).
200 /// * `parent_columns` – resolved columns belonging to the parent table.
201 /// * `child_columns` – resolved columns belonging to child table(s).
202 /// * `key_column` – SQL column name used as the grouping key; identifies
203 /// the boundary between parent groups (matches [`GroupByConfig::key_column`]).
204 ///
205 /// ## Return value
206 ///
207 /// A string containing both struct definitions (child first, parent second),
208 /// stored in [`GeneratedCode::row_struct`].
209 ///
210 /// ## Default implementation
211 ///
212 /// Returns an error:
213 /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
214 /// Backends opt in by overriding this method.
215 fn generate_grouped_structs(
216 &self,
217 _parent_struct_name: &str,
218 _child_struct_name: &str,
219 _parent_columns: &[ResolvedColumn],
220 _child_columns: &[ResolvedColumn],
221 _key_column: &str,
222 ) -> Result<String, ScytheError> {
223 Err(ScytheError::new(
224 ErrorCode::InternalError,
225 format!("grouped queries are not yet supported by the '{}' backend", self.name()),
226 ))
227 }
228
229 /// Generate the query function for a `:grouped` query.
230 ///
231 /// The function runs the flat SQL from `analyzed.sql`, decodes each row,
232 /// and folds the rows into an **order-preserving** `Vec<parent_struct_name>`
233 /// (or the language-native equivalent), grouping by `key_column` and
234 /// appending each row's child fields to the matching parent's `children`
235 /// collection.
236 ///
237 /// ## Parameters
238 ///
239 /// All inputs are bundled in [`GroupedQueryFn`]: the analyzed query, both
240 /// generated struct names, the flat and split (parent/child) column sets,
241 /// the resolved params, and the grouping key column.
242 ///
243 /// ## Return value
244 ///
245 /// A string containing the full query function definition, stored in
246 /// [`GeneratedCode::query_fn`].
247 ///
248 /// ## Default implementation
249 ///
250 /// Returns an error:
251 /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
252 /// Backends opt in by overriding this method.
253 fn generate_grouped_query_fn(&self, _request: &GroupedQueryFn<'_>) -> Result<String, ScytheError> {
254 Err(ScytheError::new(
255 ErrorCode::InternalError,
256 format!("grouped queries are not yet supported by the '{}' backend", self.name()),
257 ))
258 }
259
260 /// Apply per-backend configuration options from [[sql.gen]].
261 /// Backends override this to handle options like `row_type = "pydantic"`.
262 fn apply_options(&mut self, _options: &std::collections::HashMap<String, String>) -> Result<(), ScytheError> {
263 Ok(())
264 }
265
266 /// Database engines this backend supports.
267 /// Defaults to PostgreSQL only. Multi-DB backends override this.
268 fn supported_engines(&self) -> &[&str] {
269 &["postgresql"]
270 }
271}