This release changes how pgrx generates extension SQL.
The main point of the release is simple: schema generation now happens in a single compilation pass.
Older pgrx versions needed two builds:
- build the extension shared library
- build and run a separate
pgrx_embedbinary to collect SQL metadata
That second pass is gone now. Metadata is embedded into the shared library
during the normal build, and cargo-pgrx reads it back out afterward.
Why you should care:
- local edit/build/test loops get shorter
- CI jobs spend less time rebuilding the same extension just to extract schema
- the schema-generation pipeline is easier to understand and debug (debatable!)
The short version:
- schema generation now uses one compilation pass instead of two
- the old
pgrx_embedbuild step is gone - manual
SqlTranslatableimplementations now use associated consts - schema dependency matching now uses
TYPE_IDENT
Most extensions won't need much work. If your extension only uses ordinary
#[pg_extern], #[derive(PostgresType)], #[derive(PostgresEnum)], and the
default templates, you can probably skim this file and move on.
You should read this guide if any of the following are true:
- your crate still has
src/bin/pgrx_embed.rs - your
Cargo.tomlstill declares a[[bin]]target forpgrx_embed - you wrote
unsafe impl SqlTranslatable for ...by hand - you gate code on
cfg(pgrx_embed) - you use
extension_sql!(..., creates = [Type(T)]/[Enum(T)])with manual types
Older pgrx versions generated SQL by doing a second build, compiling a
pgrx_embed binary, and running that binary to collect metadata.
That second build is gone now. Metadata is embedded directly into the shared
library during the normal build, then cargo-pgrx reads it back out.
The whole point is to get schema generation down to one compilation pass. That means faster local iteration and shorter CI builds, especially in workspaces where the extra embed build was noticeable.
The user-visible changes are:
- the old embed binary is no longer part of the pipeline
- manual SQL metadata has to be available as compile-time constants
The new design is simpler, faster, and easier to reason about. It also means
manual SqlTranslatable impls need to move from methods to associated consts.
Delete it.
If your crate used to look like this:
[lib]
crate-type = ["cdylib", "lib"]
[[bin]]
name = "pgrx_embed_my_extension"
path = "./src/bin/pgrx_embed.rs"and:
::pgrx::pgrx_embed!();it should now look like this:
[lib]
crate-type = ["cdylib"]and the src/bin/pgrx_embed.rs file should be deleted.
Why: there is no second embed build anymore, so keeping the old bin target
doesn't help and can only confuse the migration. The current templates and
examples also no longer need "lib" in crate-type.
This is the main breaking change.
The old shape looked like this:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable,
};
pub struct MyType;
unsafe impl SqlTranslatable for MyType {
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
Ok(SqlMapping::As("my_type".into()))
}
fn return_sql() -> Result<Returns, ReturnsError> {
Ok(Returns::One(SqlMapping::As("my_type".into())))
}
}The new shape looks like this:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, ReturnsError, ReturnsRef, SqlMappingRef, SqlTranslatable,
TypeOrigin,
};
pub struct MyType;
unsafe impl SqlTranslatable for MyType {
const TYPE_IDENT: &'static str = pgrx::pgrx_resolved_type!(MyType);
const TYPE_ORIGIN: TypeOrigin = TypeOrigin::ThisExtension;
const ARGUMENT_SQL: Result<SqlMappingRef, ArgumentError> =
Ok(SqlMappingRef::literal("my_type"));
const RETURN_SQL: Result<ReturnsRef, ReturnsError> =
Ok(ReturnsRef::One(SqlMappingRef::literal("my_type")));
}Why:
TYPE_IDENTis now the dependency identity for the typeTYPE_ORIGINtells the graph builder whether the SQL type comes from this extension or from somewhere elseARGUMENT_SQLandRETURN_SQLmust be const because metadata is embedded at compile time
If you see an error like this:
error[E0046]: not all trait items implemented, missing: `TYPE_IDENT`,
`TYPE_ORIGIN`, `ARGUMENT_SQL`, `RETURN_SQL`
this is the fix.
If your wrapper maps to an existing SQL type and the argument and return SQL are
the same, you don't need to write the full unsafe impl.
Use the helper macro from pgrx::prelude::*:
use pgrx::prelude::*;
pub struct UuidWrapper;
impl_sql_translatable!(UuidWrapper, "uuid");There is also an argument-only form for pseudo-types:
use pgrx::prelude::*;
pub struct InternalArg(*mut core::ffi::c_void);
impl_sql_translatable!(InternalArg, arg_only = "internal");The helper always uses pgrx_resolved_type!(T) for TYPE_IDENT and
TypeOrigin::External for TYPE_ORIGIN. If the type is owned by this
extension, or if the argument and return SQL differ, keep writing the
SqlTranslatable impl by hand.
Don't hard-code a bare string unless you have a very specific reason and know exactly what you are doing.
This is fragile:
unsafe impl SqlTranslatable for MyType {
const TYPE_IDENT: &'static str = "MyType";
// ...
}This is correct:
unsafe impl SqlTranslatable for MyType {
const TYPE_IDENT: &'static str = pgrx::pgrx_resolved_type!(MyType);
// ...
}Why: schema matching now happens by TYPE_IDENT. The resolved form ties the
identity to the impl or derive site plus the canonical type spelling you pass to
the macro, which avoids drift between manual impls, re-exports, and
derive-generated metadata.
This matters more than it used to.
If the SQL type is created by your extension, use TypeOrigin::ThisExtension.
Example:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, ReturnsError, ReturnsRef, SqlMappingRef, SqlTranslatable,
TypeOrigin,
};
use pgrx::prelude::*;
#[derive(Debug)]
#[repr(C)]
pub struct Complex {
pub r: f64,
pub i: f64,
}
extension_sql!(
r#"CREATE TYPE complex;"#,
name = "create_complex_shell_type",
creates = [Type(Complex)]
);
unsafe impl SqlTranslatable for Complex {
const TYPE_IDENT: &'static str = pgrx::pgrx_resolved_type!(Complex);
const TYPE_ORIGIN: TypeOrigin = TypeOrigin::ThisExtension;
const ARGUMENT_SQL: Result<SqlMappingRef, ArgumentError> =
Ok(SqlMappingRef::literal("complex"));
const RETURN_SQL: Result<ReturnsRef, ReturnsError> =
Ok(ReturnsRef::One(SqlMappingRef::literal("complex")));
}Use this pattern when you are manually implementing SqlTranslatable for a
type whose SQL type is owned by this extension.
If you use #[derive(PostgresType)] or #[derive(PostgresEnum)], pgrx
generates the SqlTranslatable impl for you, so you do not need to write this
by hand.
Typical cases are:
- a hand-written custom datum type whose SQL type is created by your extension
- a type created by your own
extension_sql!block
If your Rust type maps to an existing SQL type, use TypeOrigin::External.
Implementing SqlTranslatable by itself does not emit a CREATE TYPE
statement. The only things that create a SQL type are:
- derive-generated type or enum entities
- your own SQL declarations, such as
extension_sql!(..., creates = [Type(T)])
Old code sometimes blurred two different intentions:
- define a new SQL type owned by the extension
- use a Rust wrapper whose SQL spelling should be an existing type like
uuid
Those migrate differently.
Keep the extension_sql!() block, keep the type extension-owned, and make the
SQL spelling match the type you are actually creating.
Before:
extension_sql!(
r#"CREATE TYPE uuid_wrapper;"#,
name = "create_uuid_wrapper",
creates = [Type(UuidWrapper)]
);
unsafe impl SqlTranslatable for UuidWrapper {
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
Ok(SqlMapping::As("uuid_wrapper".into()))
}
fn return_sql() -> Result<Returns, ReturnsError> {
Ok(Returns::One(SqlMapping::As("uuid_wrapper".into())))
}
}After:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, ReturnsError, ReturnsRef, SqlMappingRef, SqlTranslatable,
TypeOrigin,
};
extension_sql!(
r#"CREATE TYPE uuid_wrapper;"#,
name = "create_uuid_wrapper",
creates = [Type(UuidWrapper)]
);
pub struct UuidWrapper;
unsafe impl SqlTranslatable for UuidWrapper {
const TYPE_IDENT: &'static str = pgrx::pgrx_resolved_type!(UuidWrapper);
const TYPE_ORIGIN: TypeOrigin = TypeOrigin::ThisExtension;
const ARGUMENT_SQL: Result<SqlMappingRef, ArgumentError> =
Ok(SqlMappingRef::literal("uuid_wrapper"));
const RETURN_SQL: Result<ReturnsRef, ReturnsError> =
Ok(ReturnsRef::One(SqlMappingRef::literal("uuid_wrapper")));
}With that setup, a later #[pg_extern] that takes UuidWrapper will render its
SQL argument as uuid_wrapper, and the extension_sql!() declaration is what
creates the SQL type node in the schema graph.
Simply mark the mapping as external.
Before:
unsafe impl SqlTranslatable for UuidWrapper {
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
Ok(SqlMapping::As("uuid".into()))
}
fn return_sql() -> Result<Returns, ReturnsError> {
Ok(Returns::One(SqlMapping::As("uuid".into())))
}
}After:
use pgrx::prelude::*;
pub struct UuidWrapper;
impl_sql_translatable!(UuidWrapper, "uuid");With that setup, a later #[pg_extern] that takes UuidWrapper will render its
SQL argument as uuid. No CREATE TYPE uuid_wrapper statement is emitted,
because you are not defining a new SQL type.
Under the hood, the macro still sets TYPE_IDENT with
pgrx_resolved_type!(UuidWrapper) and marks the mapping as External.
If you had old code that did both, a CREATE TYPE uuid_wrapper declaration and
an argument_sql() that returned "uuid", decide which behavior you actually
want. That old code was trying to describe two different SQL types at once.
This is still supported. You just need the new const-based metadata.
Before:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, Returns, ReturnsError, SqlMapping, SqlTranslatable,
};
pub struct FakeAnyElement;
unsafe impl SqlTranslatable for FakeAnyElement {
fn argument_sql() -> Result<SqlMapping, ArgumentError> {
Ok(SqlMapping::As("anyelement".into()))
}
fn return_sql() -> Result<Returns, ReturnsError> {
Err(ReturnsError::Datum)
}
}After:
use pgrx::pgrx_sql_entity_graph::metadata::{
ArgumentError, ReturnsError, ReturnsRef, SqlMappingRef, SqlTranslatable,
TypeOrigin,
};
pub struct FakeAnyElement;
unsafe impl SqlTranslatable for FakeAnyElement {
const TYPE_IDENT: &'static str = pgrx::pgrx_resolved_type!(FakeAnyElement);
const TYPE_ORIGIN: TypeOrigin = TypeOrigin::External;
const ARGUMENT_SQL: Result<SqlMappingRef, ArgumentError> =
Ok(SqlMappingRef::literal("anyelement"));
const RETURN_SQL: Result<ReturnsRef, ReturnsError> =
Err(ReturnsError::Datum);
}This pattern also applies to wrappers around internal, pseudo-types, and
other existing SQL spellings.
Drop that part of the cfg.
Before:
#[cfg(all(feature = "pg_test", pgrx_embed))]
#[test]
fn round_trip() {
// ...
}After:
#[cfg(feature = "pg_test")]
#[test]
fn round_trip() {
// ...
}Use whatever remaining cfgs make sense for your crate. The important part is
that pgrx_embed is not part of the build anymore, so it should not appear in
new gating logic.
Nothing new is required here.
If your .control file already keeps module_pathname, keep it.
module_pathname = '$libdir/my_extension'If you were already using versioned shared-object mode by omitting
module_pathname, keep omitting it.
# no module_pathname entryThis release does not change the switch for that feature. It only changes how schema metadata is collected.
Example:
error[E0046]: not all trait items implemented, missing: `TYPE_IDENT`,
`TYPE_ORIGIN`, `ARGUMENT_SQL`, `RETURN_SQL`
Meaning: you still have an old-style manual SqlTranslatable impl.
Fix: move to associated consts.
Example:
function `foo` uses `MyType` as argument, but type ident `...` did not resolve
Meaning: pgrx saw a Rust type in a SQL-bearing position, but couldn't match its
TYPE_IDENT to a declared type, enum, or creates = [Type(T)]/[Enum(T)]
declaration.
Common causes:
TYPE_IDENTwas hard-coded incorrectlyTYPE_ORIGINshould have beenExternal- you forgot the matching
#[derive(PostgresType)],#[derive(PostgresEnum)], orextension_sql!(..., creates = [...])
Meaning: two different schema entities claim the same TYPE_IDENT.
Fix: make sure each Rust type maps to one SQL owner. Don't declare the same type
twice through a mix of derive metadata and custom extension_sql! declarations.
Meaning: cargo-pgrx couldn't find the embedded metadata section in the shared
library.
Common causes:
- the artifact was built with an incompatible pgrx
- the wrong binary or architecture slice was selected
- something stripped the section out of the shared library
Meaning: you used creates = [Type(T)] or creates = [Enum(T)] for a type that
should really be External.
Fix: remove the creates declaration and set TYPE_ORIGIN to External.
- delete
src/bin/pgrx_embed.rs - remove the
[[bin]]target forpgrx_embed - convert manual
SqlTranslatableimpls to associated consts - use
pgrx::pgrx_resolved_type!(T)forTYPE_IDENT - set
TYPE_ORIGINcorrectly - only use
creates = [Type(T)]/[Enum(T)]for extension-owned types - remove
cfg(pgrx_embed)from tests or helper code
- ordinary
#[pg_extern]functions still work the same way - derive-based types and enums still work the same way
- versioned shared-object mode is still controlled by
module_pathname - SQL ordering still comes from the dependency graph, you just don't need the extra embed build anymore