Skip to content

Commit

Permalink
Add a macro pg_cast for constructing PG casts from Rust functions.
Browse files Browse the repository at this point in the history
To use the new macro, add the `pg_cast` and `pg_extern` macros on
single-argument single-return value Rust function:

```rust
[pg_extern]
[pg_cast]
fn test_cast(_value: Json) -> i32 {
    0
}
```

It is possible to parametrize the cast by adding either `assignment` or
`implicit` to the `pg_cast` macro as argument:

```
[pg_cast(assignment)] -> creates a PG cast that can be used for casting
from one type to another at assignment time

[pg_cast(implicit)] -> creates a PG cast that can be used for casting
from one type to another in any context
```

TESTED=`cargo-pgrx pgrx test --features "pg13"`
  • Loading branch information
Louis Kuang committed Dec 24, 2023
1 parent f6c032e commit 4ccf619
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 4 deletions.
7 changes: 7 additions & 0 deletions pgrx-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ 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
#[proc_macro_attribute]
pub fn pg_cast(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}

/// Declare a function as `#[pg_operator]` to indicate that it represents a Postgres operator
/// `cargo pgrx schema` will automatically generate the underlying SQL
#[proc_macro_attribute]
Expand Down
6 changes: 3 additions & 3 deletions pgrx-sql-entity-graph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ pub use extension_sql::{ExtensionSql, ExtensionSqlFile, SqlDeclared};
pub use extern_args::{parse_extern_attributes, ExternArgs};
pub use mapping::RustSqlMapping;
pub use pg_extern::entity::{
PgExternArgumentEntity, PgExternEntity, PgExternReturnEntity, PgExternReturnEntityIteratedItem,
PgOperatorEntity,
PgCastEntity, PgExternArgumentEntity, PgExternEntity, PgExternReturnEntity,
PgExternReturnEntityIteratedItem, PgOperatorEntity,
};
pub use pg_extern::{NameMacro, PgExtern, PgExternArgument, PgOperator};
pub use pg_extern::{NameMacro, PgCast, PgExtern, PgExternArgument, PgOperator};
pub use pg_trigger::attribute::PgTriggerAttribute;
pub use pg_trigger::entity::PgTriggerEntity;
pub use pg_trigger::PgTrigger;
Expand Down
46 changes: 46 additions & 0 deletions pgrx-sql-entity-graph/src/pg_extern/cast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
//LICENSE
//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
//LICENSE
//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <[email protected]>
//LICENSE
//LICENSE All rights reserved.
//LICENSE
//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
/*!
`#[pg_cast]` related macro expansion for Rust to SQL translation
> Like all of the [`sql_entity_graph`][crate] APIs, this is considered **internal**
to the `pgrx` framework and very subject to change between versions. While you may use this, please do it with caution.
*/
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens, TokenStreamExt};

/// A parsed `#[pg_cast]` operator.
///
/// It is created during [`PgExtern`](crate::PgExtern) parsing.
#[derive(Debug, Clone)]
pub enum PgCast {
Default,
Assignment,
Implicit,
}

impl ToTokens for PgCast {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let quoted = match self {
PgCast::Default => quote! {
::pgrx::pgrx_sql_entity_graph::PgCastEntity::Default
},
PgCast::Assignment => quote! {
::pgrx::pgrx_sql_entity_graph::PgCastEntity::Assignment
},
PgCast::Implicit => quote! {
::pgrx::pgrx_sql_entity_graph::PgCastEntity::Implicit
},
};
tokens.append_all(quoted);
}
}
25 changes: 25 additions & 0 deletions pgrx-sql-entity-graph/src/pg_extern/entity/cast.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
//LICENSE
//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
//LICENSE
//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <[email protected]>
//LICENSE
//LICENSE All rights reserved.
//LICENSE
//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
/*!
`#[pg_extern]` related cast entities for Rust to SQL translation
> Like all of the [`sql_entity_graph`][crate] APIs, this is considered **internal**
to the `pgrx` framework and very subject to change between versions. While you may use this, please do it with caution.
*/

/// The output of a [`PgCast`](crate::PgCast) from `quote::ToTokens::to_tokens`.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum PgCastEntity {
Default,
Assignment,
Implicit,
}
108 changes: 107 additions & 1 deletion pgrx-sql-entity-graph/src/pg_extern/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ to the `pgrx` framework and very subject to change between versions. While you m
*/
mod argument;
mod cast;
mod operator;
mod returning;

pub use argument::PgExternArgumentEntity;
pub use cast::PgCastEntity;
pub use operator::PgOperatorEntity;
pub use returning::{PgExternReturnEntity, PgExternReturnEntityIteratedItem};

Expand Down Expand Up @@ -49,6 +51,7 @@ pub struct PgExternEntity {
pub extern_attrs: Vec<ExternArgs>,
pub search_path: Option<Vec<&'static str>>,
pub operator: Option<PgOperatorEntity>,
pub cast: Option<PgCastEntity>,
pub to_sql_config: ToSqlConfigEntity,
}

Expand Down Expand Up @@ -452,7 +455,6 @@ impl ToSql for PgExternEntity {
.map(|schema| format!("{}.", schema))
.unwrap_or_else(|| context.schema_prefix_for(&self_index));

eprintln!("schema={schema}");
let operator_sql = format!("\n\n\
-- {file}:{line}\n\
-- {module_path}::{name}\n\
Expand All @@ -472,6 +474,110 @@ impl ToSql for PgExternEntity {
optionals = if !optionals.is_empty() { optionals.join(",\n") + "\n" } else { "".to_string() },
);
ext_sql + &operator_sql
} else if let Some(cast) = &self.cast {
let target_arg = &self.metadata.retval;
let target_fn_arg = &self.fn_return;
let target_arg_graph_index = context
.graph
.neighbors_undirected(self_index)
.find(|neighbor| match (&context.graph[*neighbor], target_fn_arg) {
(SqlGraphEntity::Type(ty), PgExternReturnEntity::Type { ty: rty }) => {
ty.id_matches(&rty.ty_id)
}
(SqlGraphEntity::Enum(en), PgExternReturnEntity::Type { ty: rty }) => {
en.id_matches(&rty.ty_id)
}
(SqlGraphEntity::BuiltinType(defined), _) => defined == target_arg.type_name,
_ => false,
})
.ok_or_else(|| {
eyre!("Could not find source type in graph. Got: {:?}", target_arg)
})?;
let target_arg_sql = match target_arg.argument_sql {
Ok(SqlMapping::As(ref sql)) => sql.clone(),
Ok(SqlMapping::Composite { array_brackets }) => {
if array_brackets {
let composite_type = self.fn_args[0].used_ty.composite_type
.ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgrx::composite_type!()`"))?;
format!("{composite_type}[]")
} else {
self.fn_args[0].used_ty.composite_type
.ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgrx::composite_type!()`"))?.to_string()
}
}
Ok(SqlMapping::Skip) => {
return Err(eyre!("Found an skipped SQL type in a cast, this is not valid"))
}
Err(err) => return Err(err.into()),
};
let source_arg = self
.metadata
.arguments
.first()
.ok_or_else(|| eyre!("Did not find source type for cast `{}`.", self.name))?;
let source_fn_arg = self
.fn_args
.first()
.ok_or_else(|| eyre!("Did not find source type for cast `{}`.", self.name))?;
let source_arg_graph_index = context
.graph
.neighbors_undirected(self_index)
.find(|neighbor| match &context.graph[*neighbor] {
SqlGraphEntity::Type(ty) => ty.id_matches(&source_fn_arg.used_ty.ty_id),
SqlGraphEntity::Enum(en) => en.id_matches(&source_fn_arg.used_ty.ty_id),
SqlGraphEntity::BuiltinType(defined) => defined == source_arg.type_name,
_ => false,
})
.ok_or_else(|| {
eyre!("Could not find source type in graph. Got: {:?}", source_arg)
})?;
let source_arg_sql = match source_arg.argument_sql {
Ok(SqlMapping::As(ref sql)) => sql.clone(),
Ok(SqlMapping::Composite { array_brackets }) => {
if array_brackets {
let composite_type = self.fn_args[0].used_ty.composite_type
.ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgrx::composite_type!()`"))?;
format!("{composite_type}[]")
} else {
self.fn_args[0].used_ty.composite_type
.ok_or(eyre!("Found a composite type but macro expansion time did not reveal a name, use `pgrx::composite_type!()`"))?.to_string()
}
}
Ok(SqlMapping::Skip) => {
return Err(eyre!("Found an skipped SQL type in a cast, this is not valid"))
}
Err(err) => return Err(err.into()),
};
let optional = match cast {
PgCastEntity::Default => String::from(""),
PgCastEntity::Assignment => String::from(" AS ASSIGNMENT"),
PgCastEntity::Implicit => String::from(" AS IMPLICIT"),
};

let cast_sql = format!("\n\n\
-- {file}:{line}\n\
-- {module_path}::{name}\n\
CREATE CAST (\n\
\t{schema_prefix_source}{source_arg_sql} /* {source_name} */\n\
\tAS\n\
\t{schema_prefix_target}{target_arg_sql} /* {target_name} */\n\
)\n\
WITH FUNCTION {function_name}{optional};\
",
file = self.file,
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 {
ext_sql
};
Expand Down
38 changes: 38 additions & 0 deletions pgrx-sql-entity-graph/src/pg_extern/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ to the `pgrx` framework and very subject to change between versions. While you m
*/
mod argument;
mod attribute;
mod cast;
pub mod entity;
mod operator;
mod returning;
mod search_path;

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 @@ -74,6 +77,7 @@ pub struct PgExtern {
func: syn::ItemFn,
to_sql_config: ToSqlConfig,
operator: Option<PgOperator>,
cast: Option<PgCast>,
search_path: Option<SearchPathList>,
inputs: Vec<PgExternArgument>,
input_types: Vec<syn::Type>,
Expand Down Expand Up @@ -109,6 +113,7 @@ 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 @@ -118,6 +123,7 @@ impl PgExtern {
func,
to_sql_config,
operator,
cast,
search_path,
inputs,
input_types,
Expand Down Expand Up @@ -230,6 +236,36 @@ 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 Expand Up @@ -282,6 +318,7 @@ impl PgExtern {
};

let operator = self.operator.clone().into_iter();
let cast = self.cast.clone().into_iter();
let to_sql_config = match self.overridden() {
None => self.to_sql_config.clone(),
Some(content) => ToSqlConfig { content: Some(content), ..self.to_sql_config.clone() },
Expand Down Expand Up @@ -316,6 +353,7 @@ impl PgExtern {
search_path: None #( .unwrap_or_else(|| Some(vec![#search_path])) )*,
#[allow(clippy::or_fun_call)]
operator: None #( .unwrap_or_else(|| Some(#operator)) )*,
cast: None #( .unwrap_or_else(|| Some(#cast)) )*,
to_sql_config: #to_sql_config,
};
::pgrx::pgrx_sql_entity_graph::SqlGraphEntity::Function(submission)
Expand Down
1 change: 1 addition & 0 deletions pgrx-tests/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mod log_tests;
mod memcxt_tests;
mod name_tests;
mod numeric_tests;
mod pg_cast_tests;
mod pg_extern_tests;
mod pg_guard_tests;
mod pg_operator_tests;
Expand Down
Loading

0 comments on commit 4ccf619

Please sign in to comment.