Skip to main content

scythe_codegen/
backend_trait.rs

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