diff --git a/pgrx-macros/src/lib.rs b/pgrx-macros/src/lib.rs index 6dfa4b315..5da455013 100644 --- a/pgrx-macros/src/lib.rs +++ b/pgrx-macros/src/lib.rs @@ -21,7 +21,7 @@ use operators::{deriving_postgres_eq, deriving_postgres_hash, deriving_postgres_ use pgrx_sql_entity_graph as sql_gen; use sql_gen::{ parse_extern_attributes, CodeEnrichment, ExtensionSql, ExtensionSqlFile, ExternArgs, - PgAggregate, PgExtern, PostgresEnum, Schema, + PgAggregate, PgCast, PgExtern, PostgresEnum, Schema, }; mod operators; @@ -148,11 +148,64 @@ pub fn initialize(_attr: TokenStream, item: TokenStream) -> TokenStream { item } -/// Declare a function as `#[pg_cast]` to indicate that it represents a Postgres cast -/// `cargo pgrx schema` will automatically generate the underlying SQL +/** +Declare a function as `#[pg_cast]` to indicate that it represents a Postgres [cast](https://www.postgresql.org/docs/current/sql-createcast.html). + +* `assignment`: Corresponds to [`AS ASSIGNMENT`](https://www.postgresql.org/docs/current/sql-createcast.html). +* `implicit`: Corresponds to [`AS IMPLICIT`](https://www.postgresql.org/docs/current/sql-createcast.html). + +By default if no attribute is specified, the cast function can only be used in an explicit cast. + +Functions MUST accept and return exactly one value whose type MUST be a `pgrx` supported type. `pgrx` supports many PostgreSQL types by default. +New types can be defined via [`macro@PostgresType`] or [`macro@PostgresEnum`]. + +Example usage: +```rust,ignore +use pgrx::*; +#[pg_cast(implicit)] +fn cast_json_to_int(input: Json) -> i32 { todo!() } +*/ #[proc_macro_attribute] -pub fn pg_cast(_attr: TokenStream, item: TokenStream) -> TokenStream { - item +pub fn pg_cast(attr: TokenStream, item: TokenStream) -> TokenStream { + fn wrapped(attr: TokenStream, item: TokenStream) -> Result { + use syn::parse::Parser; + let mut cast = PgCast::Default; + match syn::punctuated::Punctuated::::parse_terminated.parse(attr) + { + Ok(paths) => { + if paths.len() > 1 { + panic!( + "pg_cast must take either 0 or 1 attribute. Found {}: {}", + paths.len(), + paths.to_token_stream() + ) + } else if paths.len() == 1 { + match paths.first().unwrap().segments.last().unwrap().ident.to_string().as_str() + { + "implicit" => cast = PgCast::Implicit, + "assignment" => cast = PgCast::Assignment, + other => panic!("Unrecognized pg_cast option: {}. ", other), + } + } + } + Err(err) => { + panic!("Failed to parse attribute to pg_cast: {}", err) + } + } + // `pg_cast` does not support other `pg_extern` attributes for now, pass an empty attribute token stream. + let pg_extern = PgExtern::new(TokenStream::new().into(), item.clone().into())?.0; + Ok(CodeEnrichment(pg_extern.as_cast(cast)).to_token_stream().into()) + } + + match wrapped(attr, item) { + Ok(tokens) => tokens, + Err(e) => { + let msg = e.to_string(); + TokenStream::from(quote! { + compile_error!(#msg); + }) + } + } } /// Declare a function as `#[pg_operator]` to indicate that it represents a Postgres operator diff --git a/pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs b/pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs index 168553cb1..a74f43e93 100644 --- a/pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs +++ b/pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs @@ -580,14 +580,11 @@ impl ToSql for PgExternEntity { line = self.line, name = self.name, module_path = self.module_path, - source_arg_sql = source_arg_sql, schema_prefix_source = context.schema_prefix_for(&source_arg_graph_index), source_name = source_arg.type_name, - target_arg_sql = target_arg_sql, schema_prefix_target = context.schema_prefix_for(&target_arg_graph_index), target_name = target_arg.type_name, function_name = self.name, - optional = optional, ); ext_sql + &cast_sql } else { diff --git a/pgrx-sql-entity-graph/src/pg_extern/mod.rs b/pgrx-sql-entity-graph/src/pg_extern/mod.rs index e0b5a0222..d62919e78 100644 --- a/pgrx-sql-entity-graph/src/pg_extern/mod.rs +++ b/pgrx-sql-entity-graph/src/pg_extern/mod.rs @@ -27,7 +27,6 @@ pub use argument::PgExternArgument; pub use cast::PgCast; pub use operator::PgOperator; pub use returning::NameMacro; -use syn::Expr; use crate::ToSqlConfig; use attribute::Attribute; @@ -113,7 +112,6 @@ impl PgExtern { crate::ident_is_acceptable_to_postgres(&func.sig.ident)?; } let operator = Self::operator(&func)?; - let cast = Self::cast(&func)?; let search_path = Self::search_path(&func)?; let inputs = Self::inputs(&func)?; let input_types = Self::input_types(&func)?; @@ -123,7 +121,7 @@ impl PgExtern { func, to_sql_config, operator, - cast, + cast: None, search_path, inputs, input_types, @@ -131,6 +129,13 @@ impl PgExtern { })) } + /// Returns a new instance of this `PgExtern` with `cast` overwritten to `pg_cast`. + pub fn as_cast(&self, pg_cast: PgCast) -> PgExtern { + let mut result = self.clone(); + result.cast = Some(pg_cast); + result + } + fn input_types(func: &syn::ItemFn) -> syn::Result> { func.sig .inputs @@ -236,36 +241,6 @@ impl PgExtern { Ok(skel) } - fn cast(func: &syn::ItemFn) -> syn::Result> { - let mut skel = Option::::default(); - for attr in &func.attrs { - let last_segment = attr.path.segments.last().unwrap(); - match last_segment.ident.to_string().as_str() { - "pg_cast" => { - let mut cast = PgCast::Default; - if !attr.tokens.is_empty() { - match attr.parse_args::() { - Ok(Expr::Path(p)) => { - match p.path.segments.last().unwrap().ident.to_string().as_str() { - "implicit" => cast = PgCast::Implicit, - "assignment" => cast = PgCast::Assignment, - _ => eprintln!("Unrecognized option: {}. Using default cast options.", p.path.to_token_stream()), - } - } - _ => eprintln!( - "Unable to parse attribute to pg_cast as a Rust Expr: {}. Using default cast options.", - attr.tokens - ), - } - } - skel = Some(cast); - } - _ => (), - } - } - Ok(skel) - } - fn search_path(func: &syn::ItemFn) -> syn::Result> { func.attrs .iter() diff --git a/pgrx-tests/src/tests/pg_cast_tests.rs b/pgrx-tests/src/tests/pg_cast_tests.rs index 29c7d6241..12178050c 100644 --- a/pgrx-tests/src/tests/pg_cast_tests.rs +++ b/pgrx-tests/src/tests/pg_cast_tests.rs @@ -11,10 +11,9 @@ use pgrx::prelude::*; #[pg_schema] mod pg_catalog { - use pgrx::{pg_cast, pg_extern}; + use pgrx::pg_cast; use serde_json::Value::Number; - #[pg_extern] #[pg_cast(implicit)] fn int4_from_json(value: pgrx::Json) -> i32 { if let Number(num) = &value.0 { diff --git a/pgrx-tests/tests/ui/invalid_pgcast_function.rs b/pgrx-tests/tests/ui/invalid_pgcast_function.rs new file mode 100644 index 000000000..e54a18917 --- /dev/null +++ b/pgrx-tests/tests/ui/invalid_pgcast_function.rs @@ -0,0 +1,8 @@ +use pgrx::prelude::*; + +#[pg_cast] +pub fn cast_function() -> i32 { + 0 +} + +fn main() {} \ No newline at end of file diff --git a/pgrx-tests/tests/ui/invalid_pgcast_options.rs b/pgrx-tests/tests/ui/invalid_pgcast_options.rs new file mode 100644 index 000000000..25d9918cd --- /dev/null +++ b/pgrx-tests/tests/ui/invalid_pgcast_options.rs @@ -0,0 +1,8 @@ +use pgrx::prelude::*; + +#[pg_cast(invalid_opt)] +pub fn cast_function(foo: i32) -> i32 { + foo +} + +fn main() {} \ No newline at end of file