Skip to content

Commit

Permalink
pg_cast validate attributes at macro expansion time.
Browse files Browse the repository at this point in the history
- Validate attributes at compile time before delegating to `PgExtern`
- Using `pg_cast` and `pg_extern` on the same item will result in
  duplicate definitions
- Added a `as_cast(PgCast)` option to `PgExtern` to augment `PgExtern`.
  Did not implement this as `PgExtern` attribute to avoid poluting
  `PgExtern` options
  • Loading branch information
Louis Kuang committed Jan 24, 2024
1 parent 99643d0 commit 752f3af
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 43 deletions.
63 changes: 58 additions & 5 deletions pgrx-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<TokenStream, syn::Error> {
use syn::parse::Parser;
let mut cast = PgCast::Default;
match syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::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
Expand Down
3 changes: 0 additions & 3 deletions pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
41 changes: 8 additions & 33 deletions pgrx-sql-entity-graph/src/pg_extern/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)?;
Expand All @@ -123,14 +121,21 @@ impl PgExtern {
func,
to_sql_config,
operator,
cast,
cast: None,
search_path,
inputs,
input_types,
returns,
}))
}

/// 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<Vec<syn::Type>> {
func.sig
.inputs
Expand Down Expand Up @@ -236,36 +241,6 @@ impl PgExtern {
Ok(skel)
}

fn cast(func: &syn::ItemFn) -> syn::Result<Option<PgCast>> {
let mut skel = Option::<PgCast>::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::<Expr>() {
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<Option<SearchPathList>> {
func.attrs
.iter()
Expand Down
3 changes: 1 addition & 2 deletions pgrx-tests/src/tests/pg_cast_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions pgrx-tests/tests/ui/invalid_pgcast_function.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use pgrx::prelude::*;

#[pg_cast]
pub fn cast_function() -> i32 {
0
}

fn main() {}
8 changes: 8 additions & 0 deletions pgrx-tests/tests/ui/invalid_pgcast_options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use pgrx::prelude::*;

#[pg_cast(invalid_opt)]
pub fn cast_function(foo: i32) -> i32 {
foo
}

fn main() {}

0 comments on commit 752f3af

Please sign in to comment.