use scythe_backend::manifest::BackendManifest;
use scythe_backend::naming::{
composite_type_name, enum_type_name, enum_variant_name, fn_name, to_pascal_case, to_snake_case,
};
use std::fmt::Write;
use scythe_core::analyzer::{AnalyzedQuery, CompositeInfo, EnumInfo};
use scythe_core::errors::{ErrorCode, ScytheError};
use scythe_core::parser::QueryCommand;
use crate::backend_options::reject_unknown_options;
use crate::backend_trait::{CodegenBackend, ResolvedColumn, ResolvedParam};
use crate::backends::typescript_common::parse_bool_option;
const DEFAULT_MANIFEST_TOML: &str = include_str!("../../manifests/rust-sqlx.toml");
const DEFAULT_MANIFEST_MYSQL: &str = include_str!("../../manifests/rust-sqlx.mysql.toml");
const DEFAULT_MANIFEST_MARIADB: &str = include_str!("../../manifests/rust-sqlx.mariadb.toml");
const DEFAULT_MANIFEST_REDSHIFT: &str = include_str!("../../manifests/rust-sqlx.redshift.toml");
const DEFAULT_MANIFEST_SQLITE: &str = include_str!("../../manifests/rust-sqlx.sqlite.toml");
pub struct SqlxBackend {
manifest: BackendManifest,
engine: String,
structs_only: bool,
serde: bool,
extra_derives: Vec<String>,
nested_aggregates: bool,
}
impl SqlxBackend {
pub fn new(engine: &str) -> Result<Self, ScytheError> {
match engine {
"postgresql" | "postgres" | "pg" | "mysql" | "mariadb" | "sqlite" | "sqlite3" | "redshift" => {}
_ => {
return Err(ScytheError::new(
ErrorCode::InvalidConfig,
format!("unsupported engine '{}' for rust-sqlx backend", engine),
));
}
}
let manifest = match engine {
"mysql" => super::parse_manifest(DEFAULT_MANIFEST_MYSQL)?,
"mariadb" => super::parse_manifest(DEFAULT_MANIFEST_MARIADB)?,
"redshift" => super::parse_manifest(DEFAULT_MANIFEST_REDSHIFT)?,
"sqlite" | "sqlite3" => super::parse_manifest(DEFAULT_MANIFEST_SQLITE)?,
_ => super::parse_manifest(DEFAULT_MANIFEST_TOML)?,
};
Ok(Self {
manifest,
engine: engine.to_string(),
structs_only: false,
serde: false,
extra_derives: Vec::new(),
nested_aggregates: super::engine_supports_nested_aggregates(engine),
})
}
}
impl SqlxBackend {
fn uses_inline_enums(&self) -> bool {
matches!(self.engine.as_str(), "mysql" | "mariadb" | "sqlite" | "sqlite3")
}
fn row_field_type<'a>(&self, col: &'a ResolvedColumn) -> &'a str {
if self.uses_inline_enums() && col.neutral_type.starts_with("enum::") {
if col.nullable { "Option<String>" } else { "String" }
} else {
&col.full_type
}
}
fn pool_type(&self) -> &str {
match self.engine.as_str() {
"mysql" | "mariadb" => "sqlx::MySqlPool",
"sqlite" | "sqlite3" => "sqlx::SqlitePool",
_ => "sqlx::PgPool",
}
}
fn query_result_type(&self) -> &str {
match self.engine.as_str() {
"mysql" | "mariadb" => "sqlx::mysql::MySqlQueryResult",
"sqlite" | "sqlite3" => "sqlx::sqlite::SqliteQueryResult",
_ => "sqlx::postgres::PgQueryResult",
}
}
fn derive_line(&self, base: &[&str]) -> String {
let mut derives: Vec<String> = base.iter().map(|s| s.to_string()).collect();
if self.serde {
derives.push("serde::Serialize".to_string());
derives.push("serde::Deserialize".to_string());
}
for extra in &self.extra_derives {
if !derives.iter().any(|d| d == extra) {
derives.push(extra.clone());
}
}
format!("#[derive({})]", derives.join(", "))
}
}
impl CodegenBackend for SqlxBackend {
fn name(&self) -> &str {
"rust-sqlx"
}
fn manifest(&self) -> &scythe_backend::manifest::BackendManifest {
&self.manifest
}
fn manifest_mut(&mut self) -> &mut scythe_backend::manifest::BackendManifest {
&mut self.manifest
}
fn supported_engines(&self) -> &[&str] {
&["postgresql", "mysql", "mariadb", "sqlite", "redshift"]
}
fn file_header(&self) -> String {
"#![allow(dead_code, unused_imports, clippy::needless_question_mark, clippy::redundant_closure)]".to_string()
}
fn apply_options(&mut self, options: &std::collections::HashMap<String, String>) -> Result<(), ScytheError> {
reject_unknown_options(&["structs_only", "serde", "derive"], options)?;
if let Some(value) = options.get("structs_only") {
self.structs_only = parse_bool_option("structs_only", value)?;
}
if let Some(value) = options.get("serde") {
self.serde = parse_bool_option("serde", value)?;
}
if let Some(value) = options.get("derive") {
self.extra_derives = value.split(',').map(|s| s.trim().to_string()).collect();
}
Ok(())
}
fn generate_struct_decl(
&self,
struct_name: &str,
_query_name: &str,
columns: &[ResolvedColumn],
) -> Result<String, ScytheError> {
let mut out = String::new();
let _ = writeln!(out, "{}", self.derive_line(&["Debug", "Clone", "sqlx::FromRow"]));
let _ = writeln!(out, "pub struct {} {{", struct_name);
for col in columns {
let field_type = self.row_field_type(col);
write_sqlx_rename_attr(&mut out, col);
let _ = writeln!(out, " pub {}: {},", col.field_name, field_type);
}
let _ = write!(out, "}}");
Ok(out)
}
fn generate_query_fn(
&self,
analyzed: &AnalyzedQuery,
struct_name: &str,
columns: &[ResolvedColumn],
params: &[ResolvedParam],
) -> Result<String, ScytheError> {
if self.structs_only {
return Ok(String::new());
}
let func_name = fn_name(&analyzed.name, &self.manifest.naming);
let mut out = String::new();
if let Some(ref msg) = analyzed.deprecated {
let _ = writeln!(out, "#[deprecated(note = \"{}\")]", msg);
}
let pool_type = self.pool_type();
let mut param_parts: Vec<String> = vec![format!("pool: &{}", pool_type)];
for param in params {
param_parts.push(format!("{}: {}", param.field_name, param.borrowed_type));
}
let sql_raw = super::clean_sql_with_optional(&analyzed.sql, &analyzed.optional_params, &analyzed.params);
let sql =
crate::sql_literal::escape_rust_string(&rewrite_sql_for_row_columns(&sql_raw, columns, &self.manifest));
let bind_params: String = analyzed
.params
.iter()
.map(|p| {
let param_name = to_snake_case(&p.name);
if p.neutral_type.starts_with("enum::") {
let enum_name = p.neutral_type.strip_prefix("enum::").unwrap();
let rust_type = enum_type_name(enum_name, &self.manifest.naming);
format!(", {} as &{}", param_name, rust_type)
} else {
format!(", {}", param_name)
}
})
.collect();
if matches!(analyzed.command, QueryCommand::Batch) {
let batch_fn_name = format!("{}_batch", func_name);
if params.len() > 1 {
let params_struct_name = format!("{}BatchParams", struct_name);
let _ = writeln!(out, "{}", self.derive_line(&["Debug", "Clone"]));
let _ = writeln!(out, "pub struct {} {{", params_struct_name);
for param in params {
let _ = writeln!(out, " pub {}: {},", param.field_name, param.full_type);
}
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(
out,
"pub async fn {}(pool: &{}, items: &[{}]) -> Result<(), sqlx::Error> {{",
batch_fn_name, pool_type, params_struct_name
);
let _ = writeln!(out, " let mut tx = pool.begin().await?;");
let _ = writeln!(out, " for item in items {{");
let struct_bind_params: String = params
.iter()
.map(|p| {
if p.neutral_type.starts_with("enum::") {
let enum_name = p.neutral_type.strip_prefix("enum::").unwrap();
let rust_type = enum_type_name(enum_name, &self.manifest.naming);
format!(", item.{} as &{}", p.field_name, rust_type)
} else {
format!(", item.{}", p.field_name)
}
})
.collect();
let _ = writeln!(out, " sqlx::query!(\"{}\"{})", sql, struct_bind_params);
let _ = writeln!(out, " .execute(&mut *tx)");
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " tx.commit().await?;");
let _ = writeln!(out, " Ok(())");
} else if params.len() == 1 {
let param = ¶ms[0];
let _ = writeln!(
out,
"pub async fn {}(pool: &{}, items: &[{}]) -> Result<(), sqlx::Error> {{",
batch_fn_name, pool_type, param.full_type
);
let _ = writeln!(out, " let mut tx = pool.begin().await?;");
let _ = writeln!(out, " for item in items {{");
let _ = writeln!(out, " sqlx::query!(\"{}\", item)", sql);
let _ = writeln!(out, " .execute(&mut *tx)");
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " tx.commit().await?;");
let _ = writeln!(out, " Ok(())");
} else {
let _ = writeln!(
out,
"pub async fn {}(pool: &{}, count: usize) -> Result<(), sqlx::Error> {{",
batch_fn_name, pool_type
);
let _ = writeln!(out, " let mut tx = pool.begin().await?;");
let _ = writeln!(out, " for _ in 0..count {{");
let _ = writeln!(out, " sqlx::query!(\"{}\")", sql);
let _ = writeln!(out, " .execute(&mut *tx)");
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " tx.commit().await?;");
let _ = writeln!(out, " Ok(())");
}
let _ = write!(out, "}}");
return Ok(out);
}
let return_type = match &analyzed.command {
QueryCommand::One => struct_name.to_string(),
QueryCommand::Opt => format!("Option<{}>", struct_name),
QueryCommand::Many => format!("Vec<{}>", struct_name),
QueryCommand::Exec => "()".to_string(),
QueryCommand::ExecResult => self.query_result_type().to_string(),
QueryCommand::ExecRows => "u64".to_string(),
QueryCommand::Batch => unreachable!(),
QueryCommand::Grouped => {
unreachable!("Grouped command is routed through generate_grouped_query_fn, not generate_query_fn")
}
};
let _ = writeln!(
out,
"pub async fn {}({}) -> Result<{}, sqlx::Error> {{",
func_name,
param_parts.join(", "),
return_type
);
let has_row_struct = matches!(
analyzed.command,
QueryCommand::One | QueryCommand::Many | QueryCommand::Opt
);
let is_exec_rows = matches!(analyzed.command, QueryCommand::ExecRows);
if is_exec_rows {
if has_row_struct && !analyzed.columns.is_empty() {
let _ = write!(
out,
" let result = sqlx::query_as!({}, \"{}\"{})",
struct_name, sql, bind_params
);
} else {
let _ = write!(out, " let result = sqlx::query!(\"{}\"{})", sql, bind_params);
}
} else if has_row_struct && !analyzed.columns.is_empty() {
let _ = write!(out, " sqlx::query_as!({}, \"{}\"{})", struct_name, sql, bind_params);
} else {
let _ = write!(out, " sqlx::query!(\"{}\"{})", sql, bind_params);
}
let _ = writeln!(out);
let fetch_method = match &analyzed.command {
QueryCommand::One => ".fetch_one(pool)",
QueryCommand::Opt => ".fetch_optional(pool)",
QueryCommand::Many => ".fetch_all(pool)",
QueryCommand::Exec => ".execute(pool)",
QueryCommand::ExecResult => ".execute(pool)",
QueryCommand::ExecRows => ".execute(pool)",
QueryCommand::Batch => unreachable!(),
QueryCommand::Grouped => {
unreachable!("Grouped command is routed through generate_grouped_query_fn, not generate_query_fn")
}
};
let _ = write!(out, " {}", fetch_method);
let _ = writeln!(out);
match &analyzed.command {
QueryCommand::Exec => {
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " Ok(())");
}
QueryCommand::ExecRows => {
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " Ok(result.rows_affected())");
}
_ => {
let _ = writeln!(out, " .await");
}
}
let _ = write!(out, "}}");
Ok(out)
}
fn generate_enum_def(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
let mut out = String::with_capacity(256);
let type_name = enum_type_name(&enum_info.sql_name, &self.manifest.naming);
let _ = writeln!(
out,
"{}",
self.derive_line(&["Debug", "Clone", "PartialEq", "Eq", "sqlx::Type"])
);
match self.engine.as_str() {
"mysql" | "mariadb" | "sqlite" | "sqlite3" => {
let _ = writeln!(out, "#[sqlx(rename_all = \"snake_case\")]");
}
_ => {
let _ = writeln!(
out,
"#[sqlx(type_name = \"{}\", rename_all = \"snake_case\")]",
enum_info.sql_name
);
}
}
let _ = writeln!(out, "pub enum {type_name} {{");
for value in &enum_info.values {
let variant = enum_variant_name(value, &self.manifest.naming);
let _ = writeln!(out, " {variant},");
}
let _ = write!(out, "}}");
Ok(out)
}
fn generate_grouped_structs(
&self,
parent_struct_name: &str,
child_struct_name: &str,
parent_columns: &[crate::backend_trait::ResolvedColumn],
child_columns: &[crate::backend_trait::ResolvedColumn],
_key_column: &str,
) -> Result<String, ScytheError> {
let mut out = String::new();
let _ = writeln!(out, "{}", self.derive_line(&["Debug", "Clone", "sqlx::FromRow"]));
let _ = writeln!(out, "pub struct {child_struct_name} {{");
for col in child_columns {
let field_type = self.row_field_type(col);
write_sqlx_rename_attr(&mut out, col);
let _ = writeln!(out, " pub {}: {field_type},", col.field_name);
}
let _ = writeln!(out, "}}");
let _ = writeln!(out);
let _ = writeln!(out, "{}", self.derive_line(&["Debug", "Clone"]));
let _ = writeln!(out, "pub struct {parent_struct_name} {{");
for col in parent_columns {
let field_type = self.row_field_type(col);
let _ = writeln!(out, " pub {}: {field_type},", col.field_name);
}
let _ = writeln!(out, " pub children: Vec<{child_struct_name}>,");
let _ = write!(out, "}}");
Ok(out)
}
fn generate_grouped_query_fn(
&self,
request: &crate::backend_trait::GroupedQueryFn<'_>,
) -> Result<String, ScytheError> {
let analyzed = request.analyzed;
let parent_struct_name = request.parent_struct_name;
let child_struct_name = request.child_struct_name;
let parent_columns = request.parent_columns;
let child_columns = request.child_columns;
let params = request.params;
let key_column = request.key_column;
if self.structs_only {
return Ok(String::new());
}
let func_name = fn_name(&analyzed.name, &self.manifest.naming);
let pool_type = self.pool_type();
let key_field = to_snake_case(key_column);
let mut out = String::new();
if let Some(ref msg) = analyzed.deprecated {
let _ = writeln!(out, "#[deprecated(note = \"{msg}\")]");
}
let mut param_parts: Vec<String> = vec![format!("pool: &{pool_type}")];
for param in params {
param_parts.push(format!("{}: {}", param.field_name, param.borrowed_type));
}
let sql_raw = super::clean_sql_with_optional(&analyzed.sql, &analyzed.optional_params, &analyzed.params);
let sql = crate::sql_literal::escape_rust_string(&rewrite_sql_for_row_columns(
&sql_raw,
parent_columns.iter().chain(child_columns.iter()),
&self.manifest,
));
let bind_params: String = analyzed
.params
.iter()
.map(|p| {
let pname = to_snake_case(&p.name);
if p.neutral_type.starts_with("enum::") {
let enum_name = p.neutral_type.strip_prefix("enum::").unwrap();
let rust_type = enum_type_name(enum_name, &self.manifest.naming);
format!(", {pname} as &{rust_type}")
} else {
format!(", {pname}")
}
})
.collect();
let _ = writeln!(
out,
"pub async fn {func_name}({}) -> Result<Vec<{parent_struct_name}>, sqlx::Error> {{",
param_parts.join(", ")
);
let _ = writeln!(out, " let flat_rows = sqlx::query!(\"{sql}\"{bind_params})");
let _ = writeln!(out, " .fetch_all(pool)");
let _ = writeln!(out, " .await?;");
let _ = writeln!(out, " let mut result: Vec<{parent_struct_name}> = Vec::new();");
let _ = writeln!(out, " for row in flat_rows {{");
let _ = writeln!(out, " let key = row.{key_field}.clone();");
let _ = writeln!(out, " let child = {child_struct_name} {{");
for col in child_columns {
let _ = writeln!(out, " {}: row.{},", col.field_name, col.field_name);
}
let _ = writeln!(out, " }};");
let _ = writeln!(
out,
" if let Some(parent) = result.iter_mut().rev().find(|p| p.{key_field} == key) {{"
);
let _ = writeln!(out, " parent.children.push(child);");
let _ = writeln!(out, " }} else {{");
let _ = writeln!(out, " result.push({parent_struct_name} {{");
for col in parent_columns {
let _ = writeln!(out, " {}: row.{},", col.field_name, col.field_name);
}
let _ = writeln!(out, " children: vec![child],");
let _ = writeln!(out, " }});");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " }}");
let _ = writeln!(out, " Ok(result)");
let _ = write!(out, "}}");
Ok(out)
}
fn generate_composite_def(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
use scythe_backend::types::resolve_type;
let struct_name = composite_type_name(&composite.sql_name, &self.manifest.naming);
let mut out = String::new();
let _ = writeln!(out, "{}", self.derive_line(&["Debug", "Clone", "sqlx::Type"]));
let _ = writeln!(out, "#[sqlx(type_name = \"{}\")]", composite.sql_name);
let _ = writeln!(out, "pub struct {} {{", struct_name);
for field in &composite.fields {
let rust_type = resolve_type(&field.neutral_type, &self.manifest, false)
.map(|t| t.into_owned())
.map_err(|e| {
ScytheError::new(ErrorCode::InternalError, format!("composite field type error: {}", e))
})?;
let _ = writeln!(out, " pub {}: {},", to_snake_case(&field.name), rust_type);
}
let _ = write!(out, "}}");
Ok(out)
}
fn generate_nested_struct_def(
&self,
nested: &scythe_core::analyzer::NestedStructInfo,
) -> Result<Option<String>, ScytheError> {
if !self.nested_aggregates {
return Ok(None);
}
Ok(Some(generate_nested_rust_struct(nested, &self.manifest)?))
}
fn generate_enum_def_for_nested(&self, enum_info: &EnumInfo) -> Result<String, ScytheError> {
let base = self.generate_enum_def(enum_info)?;
Ok(add_serde_to_enum(&base, enum_info, &self.manifest))
}
fn generate_composite_def_for_nested(&self, composite: &CompositeInfo) -> Result<String, ScytheError> {
let base = self.generate_composite_def(composite)?;
Ok(add_serde_to_first_derive(&base))
}
}
pub(crate) fn generate_nested_rust_struct(
nested: &scythe_core::analyzer::NestedStructInfo,
manifest: &BackendManifest,
) -> Result<String, ScytheError> {
use scythe_backend::types::resolve_type;
let struct_name = to_pascal_case(&nested.name).into_owned();
let mut out = String::new();
let _ = writeln!(out, "#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]");
let _ = writeln!(out, "pub struct {} {{", struct_name);
for field in &nested.fields {
let rust_type = resolve_type(&field.neutral_type, manifest, field.nullable)
.map(|t| t.into_owned())
.map_err(|e| {
ScytheError::new(
ErrorCode::InternalError,
format!("nested struct field type error: {}", e),
)
})?;
let field_name = to_snake_case(&field.name);
if field_name != field.name {
let _ = writeln!(out, " #[serde(rename = \"{}\")]", field.name);
}
let _ = writeln!(out, " pub {}: {},", field_name, rust_type);
}
let _ = write!(out, "}}");
Ok(out)
}
pub(crate) fn add_serde_to_first_derive(rendered: &str) -> String {
const SERDE: &str = ", serde::Serialize, serde::Deserialize)]";
let Some(line_end) = rendered.find('\n') else {
return rendered.to_string();
};
let (first, rest) = rendered.split_at(line_end);
if !first.starts_with("#[derive(") || !first.ends_with(")]") || first.contains("serde::") {
return rendered.to_string();
}
format!("{}{}{}", &first[..first.len() - 2], SERDE, rest)
}
pub(crate) fn add_serde_to_enum(rendered: &str, enum_info: &EnumInfo, manifest: &BackendManifest) -> String {
let with_derive = add_serde_to_first_derive(rendered);
let mut out = String::with_capacity(with_derive.len() + enum_info.values.len() * 32);
let mut values = enum_info.values.iter();
let mut in_body = false;
for line in with_derive.split_inclusive('\n') {
if !in_body {
out.push_str(line);
if line.trim_end().ends_with('{') {
in_body = true;
}
continue;
}
if line.trim_end() == "}" {
in_body = false;
out.push_str(line);
continue;
}
match values.next() {
Some(value) => {
let variant = enum_variant_name(value, &manifest.naming);
if variant != *value {
let _ = writeln!(out, " #[serde(rename = \"{}\")]", value);
}
out.push_str(line);
}
None => out.push_str(line),
}
}
out
}
fn write_sqlx_rename_attr(out: &mut String, col: &ResolvedColumn) {
if col.field_name != col.name {
let _ = writeln!(out, " #[sqlx(rename = \"{}\")]", col.name);
}
}
fn rewrite_sql_for_row_columns<'a>(
sql: &str,
columns: impl IntoIterator<Item = &'a ResolvedColumn>,
manifest: &BackendManifest,
) -> String {
let mut result = sql.to_string();
for col in columns {
let type_override = col.neutral_type.strip_prefix("enum::").map(|enum_name| {
let rust_type = enum_type_name(enum_name, &manifest.naming);
if col.nullable {
format!("Option<{}>", rust_type)
} else {
rust_type
}
});
if col.field_name == col.name && type_override.is_none() {
continue;
}
let Some(from_pos) = result.to_uppercase().find(" FROM ") else {
continue;
};
let select_part = &result[..from_pos];
let rest = &result[from_pos..];
let alias_body = match &type_override {
Some(ty) => format!("{}: {}", col.field_name, ty),
None => col.field_name.clone(),
};
let alias = format!("\"{}\"", alias_body);
let quoted_name = format!("\"{}\"", col.name);
let new_select = if let Some(pos) = select_part.rfind(quoted_name.as_str()) {
let mut selected = select_part.to_string();
let replacement = format!("{} AS {}", quoted_name, alias);
selected.replace_range(pos..pos + quoted_name.len(), &replacement);
selected
} else {
let replacement = format!("{} AS {}", col.name, alias);
replace_column_in_select(select_part, &col.name, &replacement)
};
result = format!("{}{}", new_select, rest);
}
result
}
fn replace_column_in_select(select: &str, col_name: &str, replacement: &str) -> String {
let mut result = select.to_string();
let patterns = [format!(", {}", col_name), format!(" {}", col_name)];
for pattern in &patterns {
if let Some(pos) = result.rfind(pattern.as_str()) {
let after = pos + pattern.len();
let next_char = result[after..].chars().next();
if next_char.is_none() || matches!(next_char, Some(' ') | Some(',') | Some('\n')) {
let prefix = &result[..pos + pattern.len() - col_name.len()];
let suffix = &result[after..];
result = format!("{}{}{}", prefix, replacement, suffix);
break;
}
}
}
result
}
#[cfg(test)]
mod option_tests {
use scythe_core::analyzer::AnalyzedColumn;
use super::*;
#[test]
fn structs_only_option_applies_when_true() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
backend
.apply_options(&std::collections::HashMap::from([(
"structs_only".to_string(),
"true".to_string(),
)]))
.unwrap();
assert!(backend.structs_only);
}
#[test]
fn structs_only_option_rejects_invalid_value() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
let result = backend.apply_options(&std::collections::HashMap::from([(
"structs_only".to_string(),
"maybe".to_string(),
)]));
assert!(result.is_err(), "expected 'maybe' to be rejected");
}
#[test]
fn apply_options_rejects_unknown_key_with_invalid_config() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
let err = backend
.apply_options(&std::collections::HashMap::from([(
"structs_onl".to_string(),
"true".to_string(),
)]))
.expect_err("structs_onl is not a known rust-sqlx option");
assert_eq!(err.code, ErrorCode::InvalidConfig);
assert!(err.message.contains("structs_onl"), "{}", err.message);
assert!(
err.message.contains("structs_only"),
"error should list the real option: {}",
err.message
);
}
#[test]
fn serde_and_derive_options_apply_together_with_structs_only() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
backend
.apply_options(&std::collections::HashMap::from([
("serde".to_string(), "true".to_string()),
("derive".to_string(), "PartialEq, Eq".to_string()),
("structs_only".to_string(), "true".to_string()),
]))
.unwrap();
assert!(backend.serde);
assert_eq!(backend.extra_derives, vec!["PartialEq".to_string(), "Eq".to_string()]);
assert!(backend.structs_only);
}
#[test]
fn derive_line_dedupes_extra_derive_matching_a_base_derive() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
backend
.apply_options(&std::collections::HashMap::from([(
"derive".to_string(),
"Debug, PartialEq".to_string(),
)]))
.unwrap();
let line = backend.derive_line(&["Debug", "Clone", "sqlx::FromRow"]);
assert_eq!(line, "#[derive(Debug, Clone, sqlx::FromRow, PartialEq)]");
}
#[test]
fn derive_line_dedupes_extra_derive_matching_the_serde_derives() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
backend
.apply_options(&std::collections::HashMap::from([
("serde".to_string(), "true".to_string()),
("derive".to_string(), "serde::Serialize".to_string()),
]))
.unwrap();
let line = backend.derive_line(&["Debug", "Clone"]);
assert_eq!(line, "#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]");
}
#[test]
fn serde_option_rejects_invalid_value() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
let result = backend.apply_options(&std::collections::HashMap::from([(
"serde".to_string(),
"maybe".to_string(),
)]));
assert!(result.is_err(), "expected 'maybe' to be rejected");
}
#[test]
fn serde_and_derive_options_generate_row_struct_with_serde_derive() {
let mut backend = SqlxBackend::new("postgresql").unwrap();
backend
.apply_options(&std::collections::HashMap::from([
("serde".to_string(), "true".to_string()),
("derive".to_string(), "PartialEq".to_string()),
]))
.unwrap();
let query = AnalyzedQuery::build(|aq| {
aq.name = "GetUser".to_string();
aq.command = QueryCommand::Opt;
aq.sql = "SELECT id, name FROM users".to_string();
aq.columns = vec![
AnalyzedColumn {
name: "id".to_string(),
neutral_type: "int32".to_string(),
nullable: false,
..Default::default()
},
AnalyzedColumn {
name: "name".to_string(),
neutral_type: "string".to_string(),
nullable: false,
..Default::default()
},
];
});
let result = crate::generate_with_backend(&query, &backend).unwrap();
let row_struct = result.row_struct.as_deref().unwrap();
assert!(
row_struct
.contains("#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize, serde::Deserialize, PartialEq)]"),
"row struct must carry the base derives plus serde and the extra derive; got:\n{row_struct}"
);
}
}