Skip to main content

scythe_codegen/
backend_trait.rs

1use scythe_backend::naming::row_struct_name;
2use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo, NestedStructInfo};
3use scythe_core::errors::{ErrorCode, ScytheError};
4
5use crate::GeneratedCode;
6
7/// Information needed to generate an RBS type signature file.
8#[derive(Debug, Clone)]
9pub struct RbsGenerationContext {
10    pub queries: Vec<RbsQueryInfo>,
11    pub enums: Vec<RbsEnumInfo>,
12}
13
14/// Per-query info for RBS generation.
15#[derive(Debug, Clone, Default)]
16pub struct RbsQueryInfo {
17    pub func_name: String,
18    pub struct_name: Option<String>,
19    /// The row's columns. For a `:grouped` query this holds the *parent*
20    /// columns only; the children live in [`Self::child_columns`].
21    pub columns: Vec<ResolvedColumn>,
22    /// A `:grouped` query's child-row columns, empty for every other command.
23    ///
24    /// Carried as its own field rather than packed into `columns` behind an
25    /// in-band marker: a sentinel smuggled through a data field is how #173
26    /// leaked `__unknown_col__` into user-visible output, and a `full_type`
27    /// that means "this is a child" rather than a type is the same shape of
28    /// mistake one refactor away from being read as a type.
29    pub child_columns: Vec<ResolvedColumn>,
30    pub params: Vec<ResolvedParam>,
31    pub command: scythe_core::parser::QueryCommand,
32}
33
34/// Per-enum info for RBS generation.
35#[derive(Debug, Clone)]
36pub struct RbsEnumInfo {
37    pub type_name: String,
38    pub values: Vec<String>,
39}
40
41/// A column with its type resolved to the target language.
42#[derive(Debug, Clone, Default)]
43pub struct ResolvedColumn {
44    pub name: String,
45    pub field_name: String,
46    pub lang_type: String,
47    pub full_type: String,
48    pub neutral_type: String,
49    pub nullable: bool,
50    /// Alias of the outer-joined relation this column came from, when the
51    /// column was widened to nullable by that join. See
52    /// [`scythe_core::analyzer::AnalyzedColumn::join_group`].
53    pub join_group: Option<String>,
54    /// Whether the column was nullable in the schema, before outer-join
55    /// widening.
56    pub nullable_before_join: bool,
57    /// The raw SQL type this column was derived from (see
58    /// [`scythe_core::analyzer::AnalyzedColumn::sql_type`]). Backends that need
59    /// to distinguish storage representations the neutral type collapses
60    /// (Oracle CLOB vs. VARCHAR2, both `neutral_type == "string"`) match on
61    /// this instead of `neutral_type`.
62    pub sql_type: String,
63}
64
65impl ResolvedColumn {
66    /// Whether this column can only be null because its outer join found no
67    /// row — making it a usable discriminant for a union.
68    pub fn is_join_discriminant(&self) -> bool {
69        self.join_group.is_some() && !self.nullable_before_join
70    }
71}
72
73/// A parameter with its type resolved to the target language.
74#[derive(Debug, Clone)]
75pub struct ResolvedParam {
76    pub name: String,
77    pub field_name: String,
78    pub lang_type: String,
79    pub full_type: String,
80    pub borrowed_type: String,
81    pub neutral_type: String,
82    pub nullable: bool,
83}
84
85/// Inputs for [`CodegenBackend::generate_grouped_query_fn`].
86///
87/// The grouped query-fn contract carries enough context (analyzed query, both
88/// struct names, the flat and split column sets, params, and the grouping key)
89/// that passing it positionally trips `clippy::too_many_arguments`. Bundling it
90/// into one struct keeps the per-language implementations uniform as backends
91/// opt in to grouped codegen.
92pub struct GroupedQueryFn<'a> {
93    /// Full analyzed query (SQL, name, params, optional_params, deprecated, …).
94    pub analyzed: &'a AnalyzedQuery,
95    /// Name of the generated parent struct.
96    pub parent_struct_name: &'a str,
97    /// Name of the generated child struct.
98    pub child_struct_name: &'a str,
99    /// All resolved columns in flat SELECT order; used for row decoding.
100    pub all_columns: &'a [ResolvedColumn],
101    /// Resolved columns belonging to the parent struct.
102    pub parent_columns: &'a [ResolvedColumn],
103    /// Resolved columns belonging to the child struct(s).
104    pub child_columns: &'a [ResolvedColumn],
105    /// Resolved query parameters.
106    pub params: &'a [ResolvedParam],
107    /// Grouping key column name in the flat result row
108    /// (matches [`scythe_core::analyzer::GroupByConfig::key_column`]).
109    pub key_column: &'a str,
110}
111
112/// Trait that all codegen backends must implement.
113pub trait CodegenBackend: Send + Sync {
114    /// The backend's name (e.g. "rust-sqlx", "rust-tokio-postgres").
115    fn name(&self) -> &str;
116
117    /// The backend's manifest (type mappings, naming conventions, etc).
118    fn manifest(&self) -> &scythe_backend::manifest::BackendManifest;
119
120    /// Mutable access to the backend's own manifest, so a per-target manifest
121    /// overlay (`manifest = "..."` in `[[sql.gen]]`) can be merged in right
122    /// after construction.
123    ///
124    /// Deliberately required rather than defaulted: backends read
125    /// `self.manifest` directly for naming and type resolution, so a default
126    /// that silently did nothing would make `manifest = "..."` a no-op for
127    /// whichever backends forgot to override it — the exact class of silent,
128    /// per-backend divergence that #82 was filed for. Making it required
129    /// turns that into a compile error.
130    fn manifest_mut(&mut self) -> &mut scythe_backend::manifest::BackendManifest;
131
132    /// The file extension for this backend's generated output, without a
133    /// leading dot.
134    ///
135    /// Defaults to the manifest's `[backend] file_extension`, which is right
136    /// for every backend that owns its manifest. It is overridable because the
137    /// `javascript-*` backends deliberately reuse the TypeScript manifests —
138    /// they differ only in emit mode, so giving them their own manifests would
139    /// duplicate every type mapping to change one field. `[backend]` is not
140    /// overridable through a manifest overlay (it is identity, not
141    /// configuration), so the extension has to be expressible here instead.
142    fn output_extension(&self) -> &str {
143        &self.manifest().backend.file_extension
144    }
145
146    /// Emit a row-shaped type declaration under an *already final* name.
147    ///
148    /// `struct_name` is the exact identifier to declare — this method must
149    /// not case-convert it, and must not append
150    /// [`NamingConfig::row_suffix`](scythe_backend::naming::NamingConfig::row_suffix)
151    /// or anything else to it. `doc_name` is prose only: the SQL-side name
152    /// ("GetActiveUsers", "User") that backends interpolate into a
153    /// docstring or moduledoc. Nothing may derive an identifier from it.
154    ///
155    /// This split is the fix for #164. Both callers below used to compute a
156    /// name each and hand it to one method that suffixed whatever it got,
157    /// so `generate_model_struct` declared `UserRow` while the query
158    /// function referenced `User`. Now the name is decided once, above the
159    /// backend, and the backend only renders it.
160    ///
161    /// ## Provided, but not optional
162    ///
163    /// The default returns [`ErrorCode::InternalError`] rather than
164    /// falling back to something plausible: a fallback that quietly
165    /// re-derived the name is precisely the defect this method exists to
166    /// remove. It is provided rather than required only so that a backend
167    /// which emits no types at all can override
168    /// [`generate_row_struct`](Self::generate_row_struct) and
169    /// [`generate_model_struct`](Self::generate_model_struct) directly and
170    /// never reach this. Any backend that does emit row types must
171    /// override this one and let both callers below default.
172    fn generate_struct_decl(
173        &self,
174        struct_name: &str,
175        doc_name: &str,
176        columns: &[ResolvedColumn],
177    ) -> Result<String, ScytheError> {
178        let _ = (struct_name, doc_name, columns);
179        Err(ScytheError::new(
180            ErrorCode::InternalError,
181            format!(
182                "backend '{}' emits row types but does not implement generate_struct_decl",
183                self.name()
184            ),
185        ))
186    }
187
188    /// Generate a row struct for a query result.
189    ///
190    /// Applies the manifest's `struct_case` and `row_suffix` to the query
191    /// name and renders through
192    /// [`generate_struct_decl`](Self::generate_struct_decl).
193    fn generate_row_struct(&self, query_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError> {
194        let struct_name = row_struct_name(query_name, &self.manifest().naming);
195        self.generate_struct_decl(&struct_name, query_name, columns)
196    }
197
198    /// Generate a model struct for a table — the type a `SELECT *` shares
199    /// across every query over that table.
200    ///
201    /// Names it through [`crate::model_struct_name`], the same function
202    /// `determine_struct_name` uses to decide what the generated query
203    /// functions *reference*, so declaration and reference cannot disagree
204    /// (#164). Backends whose model type is not a row type at all (Exposed
205    /// emits a table object) override this.
206    fn generate_model_struct(&self, table_name: &str, columns: &[ResolvedColumn]) -> Result<String, ScytheError> {
207        let struct_name = crate::model_struct_name(table_name, &self.manifest().naming);
208        self.generate_struct_decl(&struct_name, &struct_name, columns)
209    }
210
211    /// Generate a query function.
212    fn generate_query_fn(
213        &self,
214        analyzed: &AnalyzedQuery,
215        struct_name: &str,
216        columns: &[ResolvedColumn],
217        params: &[ResolvedParam],
218    ) -> Result<String, ScytheError>;
219
220    /// Generate an enum definition.
221    fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError>;
222
223    /// Generate a composite type definition.
224    fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError>;
225
226    /// Generate text that must be the literal first bytes of the generated
227    /// file — before any comment, including the provenance header that
228    /// assembly prepends ahead of [`file_header`](Self::file_header).
229    ///
230    /// Some languages have syntax that loses its meaning, or becomes a
231    /// syntax error, if anything precedes it: PHP's `<?php` tag must open
232    /// the file, and Ruby's `# frozen_string_literal: true` magic comment is
233    /// only recognized on line 1 (or line 2 after a shebang) — a comment
234    /// placed above it makes Ruby silently ignore the pragma. Backends with
235    /// such a constraint override this method; everything else that may
236    /// legally follow a leading comment (imports, docstrings, `#!`-style
237    /// banners) stays in [`file_header`](Self::file_header).
238    ///
239    /// Returns an empty string by default; backends may override.
240    fn file_preamble(&self) -> String {
241        String::new()
242    }
243
244    /// Generate a struct definition for a nested-aggregate result shape
245    /// (`json_agg(o.*)`, `row_to_json(u.*)`, ...). PostgreSQL only; see
246    /// [`scythe_core::analyzer::AnalyzedQuery::nested_structs`].
247    ///
248    /// ## Opt-in, not opt-out
249    ///
250    /// Returns `Ok(None)` by default — "I do not support this" — so a
251    /// backend is only at risk from this feature if it explicitly
252    /// overrides the method. `crates/scythe-codegen/src/lib.rs` rewrites
253    /// any column referencing a struct this returns `Ok(None)` for to a safe
254    /// fallback *before* type resolution. The default is plain `json`, so a
255    /// backend stays byte-identical to its pre-inference output; a manifest
256    /// may explicitly define the distinct `json_array` scalar when its
257    /// driver exposes an array-shaped JSON document as a structural value.
258    /// `json_array` is not the SQL-array container `array<json>`.
259    ///
260    /// Deliberately not the same shape as `generate_composite_def`, which
261    /// always returns a definition (`CompositeInfo` only ever exists because
262    /// a column already referenced a real catalog composite; there is
263    /// nothing to opt out of). Overriding to return `Ok(Some(_))` is safe
264    /// only when the backend's row-decoding path actually deserializes the
265    /// resulting JSON into the generated type — a backend whose
266    /// `json_nested<T>` container merely names the type without decoding it
267    /// would produce code that compiles and is wrong, which is worse than
268    /// not supporting it. `Err(_)` is reserved for a genuine failure (e.g. a
269    /// field's neutral type doesn't resolve) and must propagate rather than
270    /// degrade.
271    ///
272    /// An override must also gate on the *engine* it was constructed for,
273    /// not only on being a PostgreSQL backend: `rust-tokio-postgres`,
274    /// `go-pgx` and `python-psycopg3` all list `redshift` in
275    /// [`Self::supported_engines`], and Redshift has no `json_agg`.
276    fn generate_nested_struct_def(&self, _nested: &NestedStructInfo) -> Result<Option<String>, ScytheError> {
277        Ok(None)
278    }
279
280    /// Generate an enum definition for an enum reachable from a nested
281    /// aggregate's field list.
282    ///
283    /// Same enum, different requirements: a value inside a `json_agg` result
284    /// arrives as JSON and is decoded by the language's JSON library, not by
285    /// the database driver, so a backend whose ordinary
286    /// [`Self::generate_enum_def`] emits only driver traits (`sqlx::Type`,
287    /// `postgres_types`) must add the JSON ones here or the nested struct
288    /// will not satisfy its own `Deserialize` bound. Defaults to
289    /// `generate_enum_def`, which is correct for every backend that already
290    /// decodes enums as plain strings.
291    fn generate_enum_def_for_nested(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
292        self.generate_enum_def(enum_info)
293    }
294
295    /// Composite counterpart of [`Self::generate_enum_def_for_nested`], for a
296    /// composite type reachable from a nested aggregate's field list.
297    fn generate_composite_def_for_nested(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
298        self.generate_composite_def(composite)
299    }
300
301    /// Generate a file-level header (imports, docstring, etc).
302    /// Returns an empty string by default; backends may override.
303    fn file_header(&self) -> String {
304        String::new()
305    }
306
307    /// Generate a file-level header using already generated code as context.
308    ///
309    /// Backends with conditional imports can inspect generated fragments and avoid
310    /// broad engine-level import guesses. The default preserves existing behavior.
311    fn file_header_for_results(&self, _generated: &[GeneratedCode]) -> String {
312        self.file_header()
313    }
314
315    /// Generate a file-level footer (closing braces, etc).
316    /// Returns an empty string by default; backends may override.
317    fn file_footer(&self) -> String {
318        String::new()
319    }
320
321    /// Generate a class header that wraps query functions only.
322    /// When non-empty, the assembly will emit all type definitions (enums,
323    /// row structs, model structs) first, then this class header, then all
324    /// query functions, then the file footer.
325    /// Returns an empty string by default (no class wrapper).
326    fn query_class_header(&self) -> String {
327        String::new()
328    }
329
330    /// Generate code that should be emitted after the file footer.
331    /// This is useful for backends that need top-level code after a class wrapper.
332    /// For example, C# extension methods must be top-level, not nested.
333    /// Returns an empty string by default.
334    fn post_footer(&self) -> String {
335        String::new()
336    }
337
338    /// Generate an RBS type signature file for Ruby backends.
339    /// Returns `None` by default; Ruby backends override this.
340    fn generate_rbs_file(&self, _context: &RbsGenerationContext) -> Option<String> {
341        None
342    }
343
344    /// Generate parent and child structs for a `:grouped` query.
345    ///
346    /// A `:grouped` query folds flat rows from a normal SQL SELECT into a nested
347    /// parent/child structure entirely on the client side — the SQL itself is
348    /// unchanged from a regular `:many` query.
349    ///
350    /// ## Struct layout
351    ///
352    /// * **Child struct** (`child_struct_name`): contains all `child_columns`.
353    ///   Defined first in the output to avoid forward references.
354    /// * **Parent struct** (`parent_struct_name`): contains all `parent_columns`
355    ///   plus one extra field `children: Vec<child_struct_name>` (or the
356    ///   language-native equivalent collection type).
357    ///
358    /// ## Grouping semantics
359    ///
360    /// The generated query function fetches flat rows and folds them into an
361    /// **order-preserving** list of parent structs, appending each row's child
362    /// fields to the matching parent's collection. Equality on `key_column` is
363    /// the fold predicate.
364    ///
365    /// ## Parameters
366    ///
367    /// * `parent_struct_name` – fully qualified struct/class name for the parent
368    ///   (e.g. `"GetUsersWithOrdersRow"`).
369    /// * `child_struct_name` – fully qualified struct/class name for the child
370    ///   (e.g. `"GetUsersWithOrdersChildRow"`).
371    /// * `parent_columns` – resolved columns belonging to the parent table.
372    /// * `child_columns` – resolved columns belonging to child table(s).
373    /// * `key_column` – SQL column name used as the grouping key; identifies
374    ///   the boundary between parent groups (matches [`GroupByConfig::key_column`]).
375    ///
376    /// ## Return value
377    ///
378    /// A string containing both struct definitions (child first, parent second),
379    /// stored in [`GeneratedCode::row_struct`].
380    ///
381    /// ## Default implementation
382    ///
383    /// Returns an error:
384    /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
385    /// Backends opt in by overriding this method.
386    fn generate_grouped_structs(
387        &self,
388        _parent_struct_name: &str,
389        _child_struct_name: &str,
390        _parent_columns: &[ResolvedColumn],
391        _child_columns: &[ResolvedColumn],
392        _key_column: &str,
393    ) -> Result<String, ScytheError> {
394        Err(ScytheError::new(
395            ErrorCode::InternalError,
396            format!("grouped queries are not yet supported by the '{}' backend", self.name()),
397        ))
398    }
399
400    /// Generate the query function for a `:grouped` query.
401    ///
402    /// The function runs the flat SQL from `analyzed.sql`, decodes each row,
403    /// and folds the rows into an **order-preserving** `Vec<parent_struct_name>`
404    /// (or the language-native equivalent), grouping by `key_column` and
405    /// appending each row's child fields to the matching parent's `children`
406    /// collection.
407    ///
408    /// ## Parameters
409    ///
410    /// All inputs are bundled in [`GroupedQueryFn`]: the analyzed query, both
411    /// generated struct names, the flat and split (parent/child) column sets,
412    /// the resolved params, and the grouping key column.
413    ///
414    /// ## Return value
415    ///
416    /// A string containing the full query function definition, stored in
417    /// [`GeneratedCode::query_fn`].
418    ///
419    /// ## Default implementation
420    ///
421    /// Returns an error:
422    /// *"grouped queries are not yet supported by the '\<name\>' backend"*.
423    /// Backends opt in by overriding this method.
424    fn generate_grouped_query_fn(&self, _request: &GroupedQueryFn<'_>) -> Result<String, ScytheError> {
425        Err(ScytheError::new(
426            ErrorCode::InternalError,
427            format!("grouped queries are not yet supported by the '{}' backend", self.name()),
428        ))
429    }
430
431    /// Apply per-backend configuration options from [[sql.gen]].
432    ///
433    /// Backends that take options override this: parse each one, and reject
434    /// anything else via [`crate::backend_options::reject_unknown_options`]
435    /// against that backend's own known-key list -- see any TypeScript
436    /// backend's `apply_options` for the established shape.
437    ///
438    /// The default rejects every key (an empty known-key list), which is the
439    /// correct behavior for the majority of backends that take no options at
440    /// all -- and, just as importantly, is what a brand-new backend gets for
441    /// free if its author forgets to override this. Before #103 the default
442    /// was `Ok(())`, so any unrecognized key -- a typo, or a real key that
443    /// TOML happily parsed but no override read -- was silently discarded
444    /// everywhere except the 11 TypeScript backends, which called
445    /// `reject_unknown_options` explicitly. The same typo behaving
446    /// differently depending on target language was itself a trap; inverting
447    /// the default closes it for every backend at once instead of requiring
448    /// each one to opt in.
449    fn apply_options(&mut self, options: &std::collections::HashMap<String, String>) -> Result<(), ScytheError> {
450        crate::backend_options::reject_unknown_options(&[], options)
451    }
452
453    /// Database engines this backend supports.
454    /// Defaults to PostgreSQL only. Multi-DB backends override this.
455    fn supported_engines(&self) -> &[&str] {
456        &["postgresql"]
457    }
458}