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}