From 7044b45a5f45acbf1eda9f65e45aff328e8b7d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louis=20Gari=C3=A9py?= Date: Tue, 19 May 2026 21:38:26 -0400 Subject: [PATCH] feat: enforce edition and rust-version on the generated crate The generated crate now uses Rust edition 2024 and declares `rust-version = "1.85"`. Both are set by cornucopia and any value under `[manifest.package]` is ignored, since the syntax of the emitted code is tied to them. A warning is emitted when the user sets either key. Added `gen` to the reserved-keyword escape list so columns or queries named `gen` are emitted as `r#gen`, with a regression test in the keyword-escaping suite. --- benches/generated/Cargo.toml | 3 +- docs/src/configuration.md | 4 +- .../auto_build/auto_build_codegen/Cargo.toml | 3 +- .../basic_async_codegen/Cargo.toml | 3 +- .../basic_async_wasm_codegen/Cargo.toml | 3 +- .../basic_sync/basic_sync_codegen/Cargo.toml | 3 +- .../db/custom_types_codegen/Cargo.toml | 3 +- src/config.rs | 93 ++++++++++++++----- src/error.rs | 18 ++++ src/utils.rs | 8 +- tests/codegen/codegen/Cargo.toml | 3 +- tests/codegen/codegen/src/types.rs | 8 +- tests/codegen/cornucopia.toml | 1 - tests/codegen/schema.sql | 3 +- tests/codegen/src/main.rs | 5 +- 15 files changed, 122 insertions(+), 39 deletions(-) diff --git a/benches/generated/Cargo.toml b/benches/generated/Cargo.toml index de1da54d..8656e94e 100644 --- a/benches/generated/Cargo.toml +++ b/benches/generated/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "generated" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.deadpool-postgres] diff --git a/docs/src/configuration.md b/docs/src/configuration.md index 9ee1beb0..dca0a45e 100644 --- a/docs/src/configuration.md +++ b/docs/src/configuration.md @@ -80,7 +80,6 @@ name = "furinapp-queries" version = "1.0.0" description = "Today I wanted to eat a *quaso*." license = "MIT" -edition = "2021" [manifest.dependencies] serde = { version = "1.0", features = ["derive"] } @@ -89,6 +88,9 @@ my_custom_types = { path = "../types" } This gives you complete control over the generated Cargo.toml. Cornucopia will automatically merge your configuration with the required PostgreSQL dependencies based on the types found in your SQL queries. +### Rust edition and version +The `edition` and `rust-version` values on the generated crate are fixed by Cornucopia since they depend on the generated code itself. Setting these values in `[manifest.package]` has no effect. + ### Dependency merging Cornucopia automatically adds dependencies based on your PostgreSQL schema: - Core dependencies: `postgres-types`, `postgres-protocol`, `postgres` diff --git a/examples/auto_build/auto_build_codegen/Cargo.toml b/examples/auto_build/auto_build_codegen/Cargo.toml index eccabfc5..4fac90b3 100644 --- a/examples/auto_build/auto_build_codegen/Cargo.toml +++ b/examples/auto_build/auto_build_codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "auto_build_codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.deadpool-postgres] diff --git a/examples/basic_async/basic_async_codegen/Cargo.toml b/examples/basic_async/basic_async_codegen/Cargo.toml index 4c2ff00d..b9e93a2e 100644 --- a/examples/basic_async/basic_async_codegen/Cargo.toml +++ b/examples/basic_async/basic_async_codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "basic_async_codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.chrono] diff --git a/examples/basic_async_wasm/basic_async_wasm_codegen/Cargo.toml b/examples/basic_async_wasm/basic_async_wasm_codegen/Cargo.toml index 918fe0e9..f6addae1 100644 --- a/examples/basic_async_wasm/basic_async_wasm_codegen/Cargo.toml +++ b/examples/basic_async_wasm/basic_async_wasm_codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "basic_async_wasm_codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.chrono] diff --git a/examples/basic_sync/basic_sync_codegen/Cargo.toml b/examples/basic_sync/basic_sync_codegen/Cargo.toml index f808b709..25104567 100644 --- a/examples/basic_sync/basic_sync_codegen/Cargo.toml +++ b/examples/basic_sync/basic_sync_codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "basic_sync_codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.chrono] diff --git a/examples/custom_types/db/custom_types_codegen/Cargo.toml b/examples/custom_types/db/custom_types_codegen/Cargo.toml index 67fdb869..23ca8f85 100644 --- a/examples/custom_types/db/custom_types_codegen/Cargo.toml +++ b/examples/custom_types/db/custom_types_codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "custom_types_codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies.chrono] diff --git a/src/config.rs b/src/config.rs index 82c0593e..049b0daa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,8 @@ use std::{ str::FromStr, }; +use crate::error::Warning; + #[derive(Debug, Deserialize, Clone)] #[serde(default, deny_unknown_fields)] #[non_exhaustive] @@ -57,15 +59,10 @@ impl Config { let contents = fs::read_to_string(path)?; let mut config: Config = toml::from_str(&contents)?; - if config.manifest.package.is_none() { - config.manifest.package = default_manifest().package; - } + warn_on_ignored_package_fields(&contents); - if let Some(manifest) = &mut config.manifest.package - && manifest.edition == cargo_toml::Inheritable::Set(cargo_toml::Edition::E2015) - { - manifest.edition = cargo_toml::Inheritable::Set(cargo_toml::Edition::E2021); - } + let package = config.manifest.package.get_or_insert_with(default_package); + enforce_cornucopia_controlled_fields(package); config.check_deprecated_fields(); @@ -265,14 +262,42 @@ impl TypeMapping { } } -#[allow(deprecated)] -fn default_manifest() -> cargo_toml::Manifest { +/// Overwrite the fields on the generated crate's `[package]` that are +/// determined by cornucopia rather than the user. +fn enforce_cornucopia_controlled_fields(package: &mut cargo_toml::Package) { + package.edition = cargo_toml::Inheritable::Set(cargo_toml::Edition::E2024); + package.rust_version = Some(cargo_toml::Inheritable::Set("1.85".into())); +} + +/// Warn when the user has set fields in `[manifest.package]` that cornucopia +/// will silently override. We re-parse the raw TOML because the deserialized +/// `cargo_toml` types fill in defaults that look identical to explicit values. +fn warn_on_ignored_package_fields(contents: &str) { + let Ok(raw) = toml::from_str::(contents) else { + return; + }; + let Some(package) = raw.get("manifest").and_then(|m| m.get("package")) else { + return; + }; + if package.get("edition").is_some() { + Warning::IgnoredManifestEdition.emit(); + } + if package.get("rust-version").is_some() { + Warning::IgnoredManifestRustVersion.emit(); + } +} + +fn default_package() -> cargo_toml::Package { let mut package = cargo_toml::Package::new("cornucopia", "0.0.0"); - package.edition = cargo_toml::Inheritable::Set(cargo_toml::Edition::E2021); + enforce_cornucopia_controlled_fields(&mut package); package.publish = cargo_toml::Inheritable::Set(cargo_toml::Publish::Flag(false)); + package +} +#[allow(deprecated)] +fn default_manifest() -> cargo_toml::Manifest { cargo_toml::Manifest { - package: Some(package), + package: Some(default_package()), workspace: None, dependencies: Default::default(), dev_dependencies: Default::default(), @@ -333,14 +358,12 @@ impl ConfigBuilder { /// Set just the package name, keeping other package defaults pub fn name(mut self, name: impl Into) -> Self { - if let Some(package) = &mut self.config.manifest.package { - package.name = name.into(); - } else { - let mut package = cargo_toml::Package::new(name.into(), "0.1.0"); - package.edition = cargo_toml::Inheritable::Set(cargo_toml::Edition::E2021); - package.publish = cargo_toml::Inheritable::Set(cargo_toml::Publish::Flag(false)); - self.config.manifest.package = Some(package); - } + let package = self.config.manifest.package.get_or_insert_with(|| { + let mut package = default_package(); + package.version = cargo_toml::Inheritable::Set("0.1.0".to_string()); + package + }); + package.name = name.into(); self } @@ -551,14 +574,13 @@ version = "0.2" } #[test] - fn explicit_manifest_package_is_respected() { + fn user_controlled_package_fields_are_respected() { let toml_content = r#" queries = "db/queries" [manifest.package] name = "custom-name" version = "1.0.0" -edition = "2021" publish = false "#; @@ -574,4 +596,31 @@ publish = false assert_eq!(package.name, "custom-name"); assert_eq!(package.version(), "1.0.0"); } + + #[test] + fn cornucopia_controlled_package_fields_are_overridden() { + let toml_content = r#" +queries = "db/queries" + +[manifest.package] +name = "custom-name" +edition = "2021" +rust-version = "1.70" +"#; + + let mut tmpfile = tempfile::NamedTempFile::new().unwrap(); + tmpfile.write_all(toml_content.as_bytes()).unwrap(); + + let config = Config::from_file(tmpfile.path()).unwrap(); + + let package = config + .manifest + .package + .expect("package section should exist"); + assert_eq!( + package.edition, + cargo_toml::Inheritable::Set(cargo_toml::Edition::E2024) + ); + assert_eq!(package.rust_version(), Some("1.85")); + } } diff --git a/src/error.rs b/src/error.rs index 515dc057..3b5aafa1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,24 @@ pub(crate) enum Warning { ) )] NoQueries, + /// User set `manifest.package.edition`, which cornucopia controls. + #[error("`manifest.package.edition` is ignored")] + #[diagnostic( + severity(Warning), + help( + "Cornucopia controls the edition of the generated crate because the emitted code's syntax is tied to it. Remove this key from your config." + ) + )] + IgnoredManifestEdition, + /// User set `manifest.package.rust-version`, which cornucopia controls. + #[error("`manifest.package.rust-version` is ignored")] + #[diagnostic( + severity(Warning), + help( + "Cornucopia controls the MSRV of the generated crate because it is tied to the edition cornucopia emits for. Remove this key from your config." + ) + )] + IgnoredManifestRustVersion, } impl Warning { diff --git a/src/utils.rs b/src/utils.rs index 5b85a5a8..6d67c7f8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -54,10 +54,10 @@ pub(crate) fn db_err(err: &tokio_postgres::Error) -> Option<(u32, String, Option pub(crate) const STRICT_KEYWORD: [&str; 5] = ["Self", "_", "crate", "self", "super"]; /// Sorted list of rust reserved keywords -pub(crate) const KEYWORD: [&str; 53] = [ +pub(crate) const KEYWORD: [&str; 54] = [ "Self", "_", "abstract", "as", "async", "await", "become", "box", "break", "const", "continue", - "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", - "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref", - "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", + "crate", "do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", + "impl", "in", "let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", + "ref", "return", "self", "static", "struct", "super", "trait", "true", "try", "type", "typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while", "yield", ]; diff --git a/tests/codegen/codegen/Cargo.toml b/tests/codegen/codegen/Cargo.toml index 299c0de2..e75bc214 100644 --- a/tests/codegen/codegen/Cargo.toml +++ b/tests/codegen/codegen/Cargo.toml @@ -3,7 +3,8 @@ [package] name = "codegen" version = "0.0.0" -edition = "2021" +edition = "2024" +rust-version = "1.85" publish = false [dependencies] diff --git a/tests/codegen/codegen/src/types.rs b/tests/codegen/codegen/src/types.rs index e2f5f50f..b4249c16 100644 --- a/tests/codegen/codegen/src/types.rs +++ b/tests/codegen/codegen/src/types.rs @@ -1079,6 +1079,8 @@ impl<'a> postgres_types::ToSql for NightmareCompositeParams<'a> { pub struct SyntaxComposite { #[postgres(name = "async")] pub r#async: i32, + #[postgres(name = "gen")] + pub r#gen: i32, } impl<'a> postgres_types::ToSql for SyntaxComposite { fn to_sql( @@ -1086,7 +1088,7 @@ impl<'a> postgres_types::ToSql for SyntaxComposite { ty: &postgres_types::Type, out: &mut postgres_types::private::BytesMut, ) -> Result> { - let SyntaxComposite { r#async } = self; + let SyntaxComposite { r#async, r#gen } = self; let fields = match *ty.kind() { postgres_types::Kind::Composite(ref fields) => fields, _ => unreachable!(), @@ -1098,6 +1100,7 @@ impl<'a> postgres_types::ToSql for SyntaxComposite { out.extend_from_slice(&[0; 4]); let r = match field.name() { "async" => postgres_types::ToSql::to_sql(r#async, field.type_(), out), + "gen" => postgres_types::ToSql::to_sql(r#gen, field.type_(), out), _ => unreachable!(), }; let count = match r? { @@ -1120,11 +1123,12 @@ impl<'a> postgres_types::ToSql for SyntaxComposite { } match *ty.kind() { postgres_types::Kind::Composite(ref fields) => { - if fields.len() != 1 { + if fields.len() != 2 { return false; } fields.iter().all(|f| match f.name() { "async" => ::accepts(f.type_()), + "gen" => ::accepts(f.type_()), _ => false, }) } diff --git a/tests/codegen/cornucopia.toml b/tests/codegen/cornucopia.toml index cddc3080..8fb740a8 100644 --- a/tests/codegen/cornucopia.toml +++ b/tests/codegen/cornucopia.toml @@ -5,7 +5,6 @@ async = true [manifest.package] name = "codegen" version = "0.0.0" -edition = "2021" publish = false [manifest.dependencies] diff --git a/tests/codegen/schema.sql b/tests/codegen/schema.sql index 0ff73437..4743c296 100644 --- a/tests/codegen/schema.sql +++ b/tests/codegen/schema.sql @@ -224,7 +224,8 @@ CREATE TABLE schema.nightmare ( -- Syntax CREATE TYPE syntax_composite AS ( - async INT + async INT, + gen INT ); CREATE TYPE syntax_enum AS Enum('async', 'box', 'I Love Chocolate'); CREATE TABLE Syntax ( diff --git a/tests/codegen/src/main.rs b/tests/codegen/src/main.rs index 9a553c94..18f7770e 100644 --- a/tests/codegen/src/main.rs +++ b/tests/codegen/src/main.rs @@ -673,7 +673,10 @@ pub fn test_stress(client: &mut Client) { // Test keyword escaping pub fn test_keyword_escaping(client: &mut Client) { let params = TrickySql10Params { - r#async: SyntaxComposite { r#async: 34 }, + r#async: SyntaxComposite { + r#async: 34, + r#gen: 42, + }, r#enum: SyntaxEnum::r#box, }; tricky_sql10().params(client, ¶ms).unwrap();