use pgrx::datum::DatumWithOid;
use pgrx::pg_sys;
use pgrx::prelude::*;
pub mod spi;
macro_rules! log_debug {
($($arg:tt)+) => {
if $crate::config::log_level().eq_ignore_ascii_case("debug") {
::pgrx::notice!($($arg)+);
} else {
::pgrx::debug1!($($arg)+);
}
};
}
pub(crate) use log_debug;
pub fn spi_run_ddl(sql: &str) -> Result<(), String> {
use std::ffi::CString;
log_debug!(
"spi_run_ddl() called with SQL ({} chars): {}",
sql.len(),
truncate_chars(sql, 200)
);
let c_sql = CString::new(sql).map_err(|e| format!("DDL SQL contains null byte: {e}"))?;
unsafe {
let nonatomic = pg_sys::SPI_inside_nonatomic_context();
#[allow(clippy::cast_possible_wrap)]
let connect_result = pg_sys::SPI_connect_ext(if nonatomic {
pg_sys::SPI_OPT_NONATOMIC as i32
} else {
0
});
#[allow(clippy::cast_possible_wrap)]
if connect_result != pg_sys::SPI_OK_CONNECT as i32 {
error!(
"spi_run_ddl() FAILED: SPI_connect_ext returned error code: {}",
connect_result
);
}
let opts = pg_sys::SPIExecuteOptions {
read_only: false,
allow_nonatomic: nonatomic,
tcount: 0,
..pg_sys::SPIExecuteOptions::default()
};
let execute_result =
pg_sys::SPI_execute_extended(c_sql.as_ptr(), std::ptr::from_ref(&opts));
pg_sys::SPI_finish();
if execute_result < 0 {
error!(
"spi_run_ddl() FAILED: SPI_execute_extended error {} for DDL: {}",
execute_result, sql
);
}
}
log_debug!("spi_run_ddl() succeeded");
Ok(())
}
pub fn spi_get_string(query: &str) -> crate::TViewResult<Option<String>> {
Spi::connect(|client| {
let mut rows = client.select(query, Some(1), &[])?;
match rows.next() {
Some(row) => Ok(row[1].value::<String>()?),
None => Ok(None),
}
})
}
use pgrx::pg_sys::Oid;
pub fn qualified_relname_from_oid(oid: Oid) -> crate::TViewResult<String> {
unsafe {
let rel = pg_sys::get_rel_name(oid);
if rel.is_null() {
return Err(crate::TViewError::CatalogError {
operation: format!("Name relation {oid:?}"),
pg_error: "relation does not exist".to_string(),
});
}
let nsp = pg_sys::get_namespace_name(pg_sys::get_rel_namespace(oid));
let name = |ptr: *const std::ffi::c_char| {
std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned()
};
Ok(format!(
"{}.{}",
quote_ident(&name(nsp)),
quote_ident(&name(rel))
))
}
}
#[must_use]
pub fn quote_ident(name: &str) -> String {
let Ok(c) = std::ffi::CString::new(name) else {
return quote_identifier(name);
};
unsafe {
std::ffi::CStr::from_ptr(pg_sys::quote_identifier(c.as_ptr()))
.to_string_lossy()
.into_owned()
}
}
#[must_use]
pub fn qualified_type_name(typid: Oid, typmod: i32) -> String {
#[allow(clippy::cast_possible_truncation)] const FLAGS: u16 =
(pg_sys::FORMAT_TYPE_TYPEMOD_GIVEN | pg_sys::FORMAT_TYPE_FORCE_QUALIFY) as u16;
unsafe {
let name = pg_sys::format_type_extended(typid, typmod, FLAGS);
let out = std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned();
pg_sys::pfree(name.cast());
out
}
}
pub fn column_types(relid: Oid) -> crate::TViewResult<Vec<(String, String)>> {
Spi::connect(|client| {
let args = [crate::utils::spi::oid(relid)];
let mut out = Vec::new();
for row in client.select(
"SELECT attname::pg_catalog.text, atttypid, atttypmod FROM pg_catalog.pg_attribute \
WHERE attrelid = $1 AND attnum > 0 AND NOT attisdropped ORDER BY attnum",
None,
&args,
)? {
if let (Some(name), Some(typid), Some(typmod)) = (
row.get::<String>(1)?,
row.get::<Oid>(2)?,
row.get::<i32>(3)?,
) {
out.push((name, qualified_type_name(typid, typmod)));
}
}
Ok::<_, pgrx::spi::Error>(out)
})
.map_err(|e| crate::TViewError::CatalogError {
operation: format!("Read the column types of relation {relid:?}"),
pg_error: e.to_string(),
})
}
const EXT_SCHEMA: &str = "tviews";
thread_local! {
static LOGGED_ONCE: std::cell::RefCell<std::collections::HashSet<String>> =
std::cell::RefCell::new(std::collections::HashSet::new());
}
pub fn log_once(key: &str, message: &str) {
if first_time(key) {
log!("pg_tviews: {message}");
}
}
pub fn first_time(key: &str) -> bool {
LOGGED_ONCE.with(|seen| seen.borrow_mut().insert(key.to_string()))
}
pub fn forget_logged(key: &str) {
LOGGED_ONCE.with(|seen| seen.borrow_mut().remove(key));
}
pub fn meta_table() -> String {
format!("{EXT_SCHEMA}.pg_tview_meta")
}
pub const fn ext_schema() -> &'static str {
EXT_SCHEMA
}
pub fn get_view_columns_by_oid(rel_oid: Oid) -> crate::TViewResult<Vec<String>> {
crate::cache::COLUMNS.with(|m| {
m.get_or_load(rel_oid, || {
crate::metrics::metrics_api::record_catalog_lookup();
crate::cache::watch(&[rel_oid]);
Spi::connect(|client| -> crate::TViewResult<Vec<String>> {
let rows = client.select(
"SELECT a.attname::pg_catalog.text FROM pg_catalog.pg_attribute a \
WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped \
ORDER BY a.attnum",
None,
&[DatumWithOid::from(rel_oid)],
)?;
let mut columns = Vec::new();
for row in rows {
if let Some(name) = row.get::<String>(1)? {
columns.push(name);
}
}
Ok(columns)
})
})
})
}
#[must_use]
pub fn quote_identifier(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
#[must_use]
pub fn quote_literal(text: &str) -> String {
let quoted = text.replace('\'', "''");
if text.contains('\\') {
format!("E'{}'", quoted.replace('\\', "\\\\"))
} else {
format!("'{quoted}'")
}
}
pub const MAX_IDENTIFIER_BYTES: usize = 63;
#[must_use]
pub fn fit_identifier(full: String) -> String {
if full.len() <= MAX_IDENTIFIER_BYTES {
return full;
}
let hash = full.bytes().fold(0x811c_9dc5_u32, |h, b| {
(h ^ u32::from(b)).wrapping_mul(0x0100_0193)
});
let tag = format!("_{hash:08x}");
format!(
"{}{tag}",
truncate_chars(&full, MAX_IDENTIFIER_BYTES - tag.len())
)
}
pub(crate) fn truncate_chars(s: &str, max: usize) -> &str {
&s[..s.floor_char_boundary(max)]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quote_literal_reads_the_same_under_any_string_setting() {
assert_eq!(quote_literal("post"), "'post'");
assert_eq!(quote_literal("it's"), "'it''s'");
assert_eq!(quote_literal(r"a\b'c"), r"E'a\\b''c'");
}
#[test]
fn quote_identifier_always_quotes_and_doubles_quotes() {
assert_eq!(quote_identifier("post"), "\"post\"");
assert_eq!(quote_identifier("Post"), "\"Post\"");
assert_eq!(quote_identifier("test\"col"), "\"test\"\"col\"");
}
#[test]
fn test_quote_identifier_normal() {
assert_eq!(quote_identifier("post"), "\"post\"");
}
#[test]
fn test_quote_identifier_uppercase() {
assert_eq!(quote_identifier("Post"), "\"Post\"");
}
#[test]
fn test_quote_identifier_with_underscore() {
assert_eq!(quote_identifier("pk_user"), "\"pk_user\"");
}
#[test]
fn test_quote_identifier_with_internal_quotes() {
assert_eq!(quote_identifier("test\"col"), "\"test\"\"col\"");
}
}