Skip to main content

scythe_codegen/
backend_trait.rs

1use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo, NestedStructInfo};
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    /// Mutable access to the backend's own manifest, so a per-target manifest
110    /// overlay (`manifest = "..."` in `[[sql.gen]]`) can be merged in right
111    /// after construction.
112    ///
113    /// Deliberately required rather than defaulted: backends read
114    /// `self.manifest` directly for naming and type resolution, so a default
115    /// that silently did nothing would make `manifest = "..."` a no-op for
116    /// whichever backends forgot to override it — the exact class of silent,
117    /// per-backend divergence that #82 was filed for. Making it required
118    /// turns that into a compile error.
119    fn manifest_mut(&mut self) -> &mut scythe_backend::manifest::BackendManifest;
120
121    /// The file extension for this backend's generated output, without a
122    /// leading dot.
123    ///
124    /// Defaults to the manifest's `[backend] file_extension`, which is right
125    /// for every backend that owns its manifest. It is overridable because the
126    /// `javascript-*` backends deliberately reuse the TypeScript manifests —
127    /// they differ only in emit mode, so giving them their own manifests would
128    /// duplicate every type mapping to change one field. `[backend]` is not
129    /// overridable through a manifest overlay (it is identity, not
130    /// configuration), so the extension has to be expressible here instead.
131    fn output_extension(&self) -> &str {
132        &self.manifest().backend.file_extension
133    }
134
135    /// Generate a row struct for a query result.
136    fn generate_row_struct(&self, query_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError>;
137
138    /// Generate a model struct for a table.
139    fn generate_model_struct(&self, table_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError>;
140
141    /// Generate a query function.
142    fn generate_query_fn(
143        &self,
144        analyzed: &AnalyzedQuery,
145        struct_name: &str,
146        columns: &[ResolvedColumn],
147        params: &[ResolvedParam],
148    ) -> Result<String, ScytheError>;
149
150    /// Generate an enum definition.
151    fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError>;
152
153    /// Generate a composite type definition.
154    fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError>;
155
156    /// Generate text that must be the literal first bytes of the generated
157    /// file — before any comment, including the provenance header that
158    /// assembly prepends ahead of [`file_header`](Self::file_header).
159    ///
160    /// Some languages have syntax that loses its meaning, or becomes a
161    /// syntax error, if anything precedes it: PHP's `<?php` tag must open
162    /// the file, and Ruby's `# frozen_string_literal: true` magic comment is
163    /// only recognized on line 1 (or line 2 after a shebang) — a comment
164    /// placed above it makes Ruby silently ignore the pragma. Backends with
165    /// such a constraint override this method; everything else that may
166    /// legally follow a leading comment (imports, docstrings, `#!`-style
167    /// banners) stays in [`file_header`](Self::file_header).
168    ///
169    /// Returns an empty string by default; backends may override.
170    fn file_preamble(&self) -> String {
171        String::new()
172    }
173
174    /// Generate a struct definition for a nested-aggregate result shape
175    /// (`json_agg(o.*)`, `row_to_json(u.*)`, ...). PostgreSQL only; see
176    /// [`scythe_core::analyzer::AnalyzedQuery::nested_structs`].
177    ///
178    /// ## Opt-in, not opt-out
179    ///
180    /// Returns `Ok(None)` by default — "I do not support this" — so a
181    /// backend is only at risk from this feature if it explicitly
182    /// overrides the method. `crates/scythe-codegen/src/lib.rs` rewrites
183    /// any column referencing a struct this returns `Ok(None)` for back to
184    /// plain `json` *before* type resolution, so the default keeps a
185    /// non-opted-in backend's output byte-identical to what it produced
186    /// before nested-aggregate inference existed.
187    ///
188    /// Deliberately not the same shape as `generate_composite_def`, which
189    /// always returns a definition (`CompositeInfo` only ever exists because
190    /// a column already referenced a real catalog composite; there is
191    /// nothing to opt out of). Overriding to return `Ok(Some(_))` is safe
192    /// only when the backend's row-decoding path actually deserializes the
193    /// resulting JSON into the generated type — a backend whose
194    /// `json_nested<T>` container merely names the type without decoding it
195    /// would produce code that compiles and is wrong, which is worse than
196    /// not supporting it. `Err(_)` is reserved for a genuine failure (e.g. a
197    /// field's neutral type doesn't resolve) and must propagate rather than
198    /// degrade.
199    ///
200    /// An override must also gate on the *engine* it was constructed for,
201    /// not only on being a PostgreSQL backend: `rust-tokio-postgres`,
202    /// `go-pgx` and `python-psycopg3` all list `redshift` in
203    /// [`Self::supported_engines`], and Redshift has no `json_agg`.
204    fn generate_nested_struct_def(&self, _nested: &NestedStructInfo) -> Result<Option<String>, ScytheError> {
205        Ok(None)
206    }
207
208    /// Generate an enum definition for an enum reachable from a nested
209    /// aggregate's field list.
210    ///
211    /// Same enum, different requirements: a value inside a `json_agg` result
212    /// arrives as JSON and is decoded by the language's JSON library, not by
213    /// the database driver, so a backend whose ordinary
214    /// [`Self::generate_enum_def`] emits only driver traits (`sqlx::Type`,
215    /// `postgres_types`) must add the JSON ones here or the nested struct
216    /// will not satisfy its own `Deserialize` bound. Defaults to
217    /// `generate_enum_def`, which is correct for every backend that already
218    /// decodes enums as plain strings.
219    fn generate_enum_def_for_nested(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
220        self.generate_enum_def(enum_info)
221    }
222
223    /// Composite counterpart of [`Self::generate_enum_def_for_nested`], for a
224    /// composite type reachable from a nested aggregate's field list.
225    fn generate_composite_def_for_nested(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
226        self.generate_composite_def(composite)
227    }
228
229    /// Generate a file-level header (imports, docstring, etc).
230    /// Returns an empty string by default; backends may override.
231    fn file_header(&self) -> String {
232        String::new()
233    }
234
235    /// Generate a file-level header using already generated code as context.
236    ///
237    /// Backends with conditional imports can inspect generated fragments and avoid
238    /// broad engine-level import guesses. The default preserves existing behavior.
239    fn file_header_for_results(&self, _generated: &[GeneratedCode]) -> String {
240        self.file_header()
241    }
242
243    /// Generate a file-level footer (closing braces, etc).
244    /// Returns an empty string by default; backends may override.
245    fn file_footer(&self) -> String {
246        String::new()
247    }
248
249    /// Generate a class header that wraps query functions only.
250    /// When non-empty, the assembly will emit all type definitions (enums,
251    /// row structs, model structs) first, then this class header, then all
252    /// query functions, then the file footer.
253    /// Returns an empty string by default (no class wrapper).
254    fn query_class_header(&self) -> String {
255        String::new()
256    }
257
258    /// Generate code that should be emitted after the file footer.
259    /// This is useful for backends that need top-level code after a class wrapper.
260    /// For example, C# extension methods must be top-level, not nested.
261    /// Returns an empty string by default.
262    fn post_footer(&self) -> String {
263        String::new()
264    }
265
266    /// Generate an RBS type signature file for Ruby backends.
267    /// Returns `None` by default; Ruby backends override this.
268    fn generate_rbs_file(&self, _context: &RbsGenerationContext) -> Option<String> {
269        None
270    }
271
272    /// Generate parent and child structs for a `:grouped` query.
273    ///
274    /// A `:grouped` query folds flat rows from a normal SQL SELECT into a nested
275    /// parent/child structure entirely on the client side — the SQL itself is
276    /// unchanged from a regular `:many` query.
277    ///
278    /// ## Struct layout
279    ///
280    /// * **Child struct** (`child_struct_name`): contains all `child_columns`.
281    ///   Defined first in the output to avoid forward references.
282    /// * **Parent struct** (`parent_struct_name`): contains all `parent_columns`
283    ///   plus one extra field `children: Vec<child_struct_name>` (or the
284    ///   language-native equivalent collection type).
285    ///
286    /// ## Grouping semantics
287    ///
288    /// The generated query function fetches flat rows and folds them into an
289    /// **order-preserving** list of parent structs, appending each row's child
290    /// fields to the matching parent's collection. Equality on `key_column` is
291    /// the fold predicate.
292    ///
293    /// ## Parameters
294    ///
295    /// * `parent_struct_name` – fully qualified struct/class name for the parent
296    ///   (e.g. `"GetUsersWithOrdersRow"`).
297    /// * `child_struct_name` – fully qualified struct/class name for the child
298    ///   (e.g. `"GetUsersWithOrdersChildRow"`).
299    /// * `parent_columns` – resolved columns belonging to the parent table.
300    /// * `child_columns` – resolved columns belonging to child table(s).
301    /// * `key_column` – SQL column name used as the grouping key; identifies
302    ///   the boundary between parent groups (matches [`GroupByConfig::key_column`]).
303    ///
304    /// ## Return value
305    ///
306    /// A string containing both struct definitions (child first, parent second),
307    /// stored in [`GeneratedCode::row_struct`].
308    ///
309    /// ## Default implementation
310    ///
311    /// Returns an error:
312    /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
313    /// Backends opt in by overriding this method.
314    fn generate_grouped_structs(
315        &self,
316        _parent_struct_name: &str,
317        _child_struct_name: &str,
318        _parent_columns: &[ResolvedColumn],
319        _child_columns: &[ResolvedColumn],
320        _key_column: &str,
321    ) -> Result<String, ScytheError> {
322        Err(ScytheError::new(
323            ErrorCode::InternalError,
324            format!("grouped queries are not yet supported by the '{}' backend", self.name()),
325        ))
326    }
327
328    /// Generate the query function for a `:grouped` query.
329    ///
330    /// The function runs the flat SQL from `analyzed.sql`, decodes each row,
331    /// and folds the rows into an **order-preserving** `Vec<parent_struct_name>`
332    /// (or the language-native equivalent), grouping by `key_column` and
333    /// appending each row's child fields to the matching parent's `children`
334    /// collection.
335    ///
336    /// ## Parameters
337    ///
338    /// All inputs are bundled in [`GroupedQueryFn`]: the analyzed query, both
339    /// generated struct names, the flat and split (parent/child) column sets,
340    /// the resolved params, and the grouping key column.
341    ///
342    /// ## Return value
343    ///
344    /// A string containing the full query function definition, stored in
345    /// [`GeneratedCode::query_fn`].
346    ///
347    /// ## Default implementation
348    ///
349    /// Returns an error:
350    /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
351    /// Backends opt in by overriding this method.
352    fn generate_grouped_query_fn(&self, _request: &GroupedQueryFn<'_>) -> Result<String, ScytheError> {
353        Err(ScytheError::new(
354            ErrorCode::InternalError,
355            format!("grouped queries are not yet supported by the '{}' backend", self.name()),
356        ))
357    }
358
359    /// Apply per-backend configuration options from [[sql.gen]].
360    ///
361    /// Backends that take options override this: parse each one, and reject
362    /// anything else via [`crate::backend_options::reject_unknown_options`]
363    /// against that backend's own known-key list -- see any TypeScript
364    /// backend's `apply_options` for the established shape.
365    ///
366    /// The default rejects every key (an empty known-key list), which is the
367    /// correct behavior for the majority of backends that take no options at
368    /// all -- and, just as importantly, is what a brand-new backend gets for
369    /// free if its author forgets to override this. Before #103 the default
370    /// was `Ok(())`, so any unrecognized key -- a typo, or a real key that
371    /// TOML happily parsed but no override read -- was silently discarded
372    /// everywhere except the 11 TypeScript backends, which called
373    /// `reject_unknown_options` explicitly. The same typo behaving
374    /// differently depending on target language was itself a trap; inverting
375    /// the default closes it for every backend at once instead of requiring
376    /// each one to opt in.
377    fn apply_options(&mut self, options: &std::collections::HashMap<String, String>) -> Result<(), ScytheError> {
378        crate::backend_options::reject_unknown_options(&[], options)
379    }
380
381    /// Database engines this backend supports.
382    /// Defaults to PostgreSQL only. Multi-DB backends override this.
383    fn supported_engines(&self) -> &[&str] {
384        &["postgresql"]
385    }
386}