Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions supabase-wrappers/src/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Checks foreign table column types against what `Cell` can represent.

use pgrx::PgSqlErrorCode;
use pgrx::pg_sys::panic::ErrorReport;
use pgrx::prelude::*;
use pgrx::rel::PgRelation;
use std::ffi::CStr;

// Must match what Cell::into_datum()/from_polymorphic_datum() (interface.rs) actually
// handle: into_datum() writes straight into the output slot with no type check, so it
// only needs binary compatibility (text/varchar/bpchar share the varlena layout); but
// from_polymorphic_datum() matches on exact OID, so rowid/qual columns need a real arm
// there too or they silently parse as None instead of erroring.
const SUPPORTED_TYPE_OIDS: &[pg_sys::Oid] = &[
pg_sys::BOOLOID,
pg_sys::CHAROID,
pg_sys::INT2OID,
pg_sys::FLOAT4OID,
pg_sys::INT4OID,
pg_sys::FLOAT8OID,
pg_sys::INT8OID,
pg_sys::NUMERICOID,
pg_sys::TEXTOID,
pg_sys::VARCHAROID,
pg_sys::BPCHAROID,
pg_sys::DATEOID,
pg_sys::TIMEOID,
pg_sys::TIMESTAMPOID,
pg_sys::TIMESTAMPTZOID,
pg_sys::INTERVALOID,
pg_sys::JSONBOID,
pg_sys::BYTEAOID,
pg_sys::UUIDOID,
pg_sys::BOOLARRAYOID,
pg_sys::INT2ARRAYOID,
pg_sys::INT4ARRAYOID,
pg_sys::INT8ARRAYOID,
pg_sys::FLOAT4ARRAYOID,
pg_sys::FLOAT8ARRAYOID,
pg_sys::TEXTARRAYOID,
pg_sys::VARCHARARRAYOID,
pg_sys::BPCHARARRAYOID,
];

const SUPPORTED_TYPES_HINT: &str = "supported column types are: boolean, \"char\", smallint, \
integer, bigint, real, double precision, numeric, text, character varying, character, date, \
time, timestamp, timestamp with time zone, interval, jsonb, bytea, uuid, arrays of these, \
and domains over any of these";

/// Resolves domains to their base type first, so e.g. `CREATE DOMAIN my_text AS text` passes.
fn is_supported_type(typoid: pg_sys::Oid) -> bool {
let base = unsafe { pg_sys::getBaseType(typoid) };
SUPPORTED_TYPE_OIDS.contains(&base)
}

#[derive(thiserror::Error, Debug)]
pub enum AttrsError {
#[error("foreign table \"{table}\" has columns with unsupported data types: {columns}")]
UnsupportedColumnTypes { table: String, columns: String },
}

impl From<AttrsError> for ErrorReport {
fn from(value: AttrsError) -> Self {
let message = format!("{value}");
ErrorReport::new(
PgSqlErrorCode::ERRCODE_FDW_INVALID_DATA_TYPE,
message,
SUPPORTED_TYPES_HINT,
)
}
}

/// Checks every non-dropped column of `relid`, collecting all offending columns into a
/// single error rather than stopping at the first one.
pub fn check_foreign_table_column_types(relid: pg_sys::Oid) -> Result<(), AttrsError> {
let relation = unsafe { PgRelation::open(relid) };
let tuple_desc = relation.tuple_desc();

let mut bad_columns = Vec::new();
for attr in tuple_desc.iter().filter(|a| !a.is_dropped()) {
if is_supported_type(attr.atttypid) {
continue;
}
let type_name = unsafe {
CStr::from_ptr(pg_sys::format_type_be(attr.atttypid))
.to_string_lossy()
.into_owned()
};
bad_columns.push(format!("\"{}\" (type {type_name})", attr.name()));
}

if bad_columns.is_empty() {
return Ok(());
}

Err(AttrsError::UnsupportedColumnTypes {
table: relation.name().to_string(),
columns: bad_columns.join(", "),
})
}
16 changes: 16 additions & 0 deletions supabase-wrappers/src/event_triggers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Generic support for writing event trigger handlers in Rust (`pgrx` has `trigger_support`
//! for row triggers, but no equivalent for event triggers).

use pgrx::nodes::is_a;
use pgrx::prelude::*;

/// Mirrors `pg_sys::called_as_trigger`, for event triggers instead of row triggers.
///
/// # Safety
///
/// `fcinfo` must be a valid `pg_sys::FunctionCallInfo` for the duration of the call.
pub unsafe fn called_as_event_trigger(fcinfo: pg_sys::FunctionCallInfo) -> bool {
let fcinfo = unsafe { fcinfo.as_ref().expect("fcinfo was null") };
!fcinfo.context.is_null()
&& unsafe { is_a(fcinfo.context, pg_sys::NodeTag::T_EventTriggerData) }
}
10 changes: 8 additions & 2 deletions supabase-wrappers/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,11 @@ impl FromDatum for Cell {
PgOid::BuiltIn(PgBuiltInOids::NUMERICOID) => {
AnyNumeric::from_datum(datum, is_null).map(Cell::Numeric)
}
PgOid::BuiltIn(PgBuiltInOids::TEXTOID) => {
PgOid::BuiltIn(PgBuiltInOids::TEXTOID)
| PgOid::BuiltIn(PgBuiltInOids::VARCHAROID)
| PgOid::BuiltIn(PgBuiltInOids::BPCHAROID) => {
// `text`, `varchar` and `bpchar` all share the same varlena
// representation, so it's safe to read any of them as a `String`.
String::from_datum(datum, is_null).map(Cell::String)
}
PgOid::BuiltIn(PgBuiltInOids::DATEOID) => {
Expand Down Expand Up @@ -361,7 +365,9 @@ impl FromDatum for Cell {
PgOid::BuiltIn(PgBuiltInOids::FLOAT8ARRAYOID) => {
Vec::<Option<f64>>::from_datum(datum, false).map(Cell::F64Array)
}
PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID) => {
PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID)
| PgOid::BuiltIn(PgBuiltInOids::VARCHARARRAYOID)
| PgOid::BuiltIn(PgBuiltInOids::BPCHARARRAYOID) => {
Vec::<Option<String>>::from_datum(datum, false).map(Cell::StringArray)
}
PgOid::Custom(_) => {
Expand Down
2 changes: 2 additions & 0 deletions supabase-wrappers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,8 @@
//! - [SQL Server](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/mssql_fdw): A FDW for [Microsoft SQL Server](https://www.microsoft.com/en-au/sql-server/) which supports data read only.
//! - [Redis](https://github.com/supabase/wrappers/tree/main/wrappers/src/fdw/redis_fdw): A FDW for [Redis](https://redis.io/) which supports data read only.

pub mod attrs;
pub mod event_triggers;
pub mod interface;
pub mod options;
pub mod qual;
Expand Down
54 changes: 54 additions & 0 deletions wrappers/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn main() {
// otherwise leading it to report the include!() in s3vec.rs as unresolvable
// even though the module is only ever compiled when the feature is on.
generate_s3vec_type_sql();
generate_event_triggers_sql();
}

/// Generates `s3vec_type_sql.rs` in OUT_DIR, which contains the `pgrx::extension_sql!` call
Expand Down Expand Up @@ -130,3 +131,56 @@ $s3vec_upgrade$;
fs::write(&out_path, content)
.unwrap_or_else(|e| panic!("failed to write {}: {e}", out_path.display()));
}

/// Generates `event_triggers_sql.rs` in OUT_DIR, which contains the `pgrx::extension_sql!`
/// call registering `check_supported_column_types` (the event trigger from
/// `event_triggers.rs`) with the correct versioned library name embedded as a string
/// literal. Same indirection as `generate_s3vec_type_sql`, same reason.
///
/// The generated file is included in `event_triggers.rs` via:
/// `include!(concat!(env!("OUT_DIR"), "/event_triggers_sql.rs"));`
fn generate_event_triggers_sql() {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let pkg_name = std::env::var("CARGO_PKG_NAME").expect("CARGO_PKG_NAME not set");
let pkg_version = std::env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION not set");
let lib_name = format!("{pkg_name}-{pkg_version}");

// The outer r##"..."## delimiter allows r#"..."# to appear inside the format string.
let content = format!(
r##"// @generated by build.rs — do not edit by hand.
// Library name "{lib_name}" is embedded at compile time from CARGO_PKG_NAME/VERSION.
pgrx::extension_sql!(
r#"DO $register_event_triggers$
BEGIN

-- 1. Check supported types function
-- no OR REPLACE guard needed (never raises duplicate_function); no
-- IMMUTABLE/STRICT/PARALLEL SAFE either, since none of them are true here
CREATE OR REPLACE FUNCTION "check_supported_column_types"()
RETURNS event_trigger
LANGUAGE c
AS '{lib_name}', 'check_supported_column_types';

-- 2. Register event trigger
-- 'ALTER TABLE' is needed too: Postgres tags `ALTER TABLE <foreign_table> ADD COLUMN`
-- (the common way to alter a foreign table) as 'ALTER TABLE', not 'ALTER FOREIGN TABLE'.
BEGIN
CREATE EVENT TRIGGER check_supported_column_types
ON ddl_command_end
WHEN TAG IN ('CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE', 'ALTER TABLE')
EXECUTE FUNCTION check_supported_column_types();
EXCEPTION WHEN duplicate_object THEN NULL;
END;
END
$register_event_triggers$;
"#,
name = "register_event_triggers",
creates = [Function(check_supported_column_types)],
);
"##
);

let out_path = Path::new(&out_dir).join("event_triggers_sql.rs");
fs::write(&out_path, content)
.unwrap_or_else(|e| panic!("failed to write {}: {e}", out_path.display()));
}
Loading