use std::collections::HashMap;
use std::error::Error;
pub trait PostgresEnum: Sized {
const PG_TYPE_NAME: &'static str;
fn to_label(&self) -> &'static str;
fn from_label(label: &str) -> Result<Self, Box<dyn Error + Sync + Send>>;
}
#[derive(Debug, Clone)]
pub struct EnumOidEntry {
pub oid: i32,
pub array_oid: i32,
pub schema: String,
}
#[derive(Debug, Clone, Default)]
pub struct EnumTypeRegistry {
by_oid: HashMap<i32, Vec<String>>,
array_oids: HashMap<i32, i32>,
}
impl EnumTypeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, name: String, entry: EnumOidEntry) {
self.by_oid.entry(entry.oid).or_default().push(name);
if entry.array_oid != 0 {
self.array_oids.insert(entry.array_oid, entry.oid);
}
}
pub fn has_oid_for_type(&self, name: &str, oid: i32) -> bool {
self.by_oid
.get(&oid)
.is_some_and(|names| names.iter().any(|n| n == name))
}
pub fn is_enum_array_oid(&self, oid: i32) -> bool {
self.array_oids.contains_key(&oid)
}
pub fn element_oid_for_array_oid(&self, array_oid: i32) -> Option<i32> {
self.array_oids.get(&array_oid).copied()
}
pub fn is_empty(&self) -> bool {
self.by_oid.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_registry_has_oid_for_type() {
let mut registry = EnumTypeRegistry::new();
registry.insert(
"mood".to_string(),
EnumOidEntry {
oid: 16384,
array_oid: 16385,
schema: "public".to_string(),
},
);
assert!(registry.has_oid_for_type("mood", 16384));
assert!(!registry.has_oid_for_type("mood", 99999));
assert!(!registry.has_oid_for_type("other", 16384));
}
#[test]
fn test_registry_multiple_schemas() {
let mut registry = EnumTypeRegistry::new();
registry.insert(
"mood".to_string(),
EnumOidEntry {
oid: 16384,
array_oid: 16385,
schema: "public".to_string(),
},
);
registry.insert(
"mood".to_string(),
EnumOidEntry {
oid: 16400,
array_oid: 16401,
schema: "other_schema".to_string(),
},
);
assert!(registry.has_oid_for_type("mood", 16384));
assert!(registry.has_oid_for_type("mood", 16400));
assert!(!registry.has_oid_for_type("mood", 99999));
}
#[test]
fn test_registry_array_oid() {
let mut registry = EnumTypeRegistry::new();
registry.insert(
"mood".to_string(),
EnumOidEntry {
oid: 16384,
array_oid: 16385,
schema: "public".to_string(),
},
);
assert!(registry.is_enum_array_oid(16385));
assert!(!registry.is_enum_array_oid(16384));
assert!(!registry.is_enum_array_oid(99999));
}
#[cfg(all(feature = "tokio", feature = "derive"))]
mod tokio_connection {
use crate::test_helpers::get_settings;
use crate::tokio_connection::TokioConnectionFactory;
use crate::{PostgresEnum, PostgresPool};
use tokio::test;
#[derive(Debug, Clone, PartialEq, PostgresEnum)]
#[postgres(name = "mood")]
enum Mood {
Happy,
Sad,
Neutral,
}
#[derive(Debug, Clone, PartialEq, PostgresEnum)]
enum UserRole {
Admin,
#[postgres(label = "regular_user")]
Regular,
Guest,
}
async fn setup_enum_client() -> crate::pool::PoolableClient<TokioConnectionFactory> {
let mut plain_client = crate::tokio_connection::new_client(get_settings())
.await
.unwrap();
plain_client
.execute_non_query_simple(
"DROP TYPE IF EXISTS mood CASCADE;
CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral');
DROP TYPE IF EXISTS user_role CASCADE;
CREATE TYPE user_role AS ENUM ('admin', 'regular_user', 'guest');
DROP TABLE IF EXISTS enum_test;
CREATE TABLE enum_test (id serial PRIMARY KEY, m mood, r user_role);",
)
.await
.unwrap();
drop(plain_client);
let settings = get_settings()
.register_enum::<Mood>()
.register_enum::<UserRole>();
let pool = PostgresPool::new(TokioConnectionFactory, settings)
.await
.unwrap();
assert!(
!pool.enum_registry().is_empty(),
"Enum registry should not be empty after registering enums"
);
pool.get_client().await.unwrap()
}
#[test]
async fn test_enum_text_mode() {
let mut client = setup_enum_client().await;
let value: Mood = client
.read_single_value_simple("SELECT 'happy'::mood")
.await;
assert_eq!(value, Mood::Happy);
let value: Mood = client.read_single_value_simple("SELECT 'sad'::mood").await;
assert_eq!(value, Mood::Sad);
}
#[test]
async fn test_enum_binary_mode() {
let mut client = setup_enum_client().await;
let value: Mood = client
.read_single_value("SELECT 'neutral'::mood", &[])
.await;
assert_eq!(value, Mood::Neutral);
}
#[test]
async fn test_enum_as_parameter() {
let mut client = setup_enum_client().await;
client
.execute_non_query_simple(
"DELETE FROM enum_test;
INSERT INTO enum_test (m, r) VALUES ('happy', 'admin');",
)
.await
.unwrap();
client
.execute_non_query(
"UPDATE enum_test SET m = $1 WHERE r = $2",
&[&Mood::Sad, &UserRole::Admin],
)
.await
.unwrap();
let value: Mood = client
.read_single_value("SELECT m FROM enum_test WHERE r = 'admin'::user_role", &[])
.await;
assert_eq!(value, Mood::Sad);
}
#[test]
async fn test_enum_nullable() {
let mut client = setup_enum_client().await;
client
.execute_non_query_simple(
"DELETE FROM enum_test;
INSERT INTO enum_test (m, r) VALUES (NULL, 'guest');",
)
.await
.unwrap();
let value: Option<Mood> = client
.read_single_value("SELECT m FROM enum_test WHERE r = 'guest'::user_role", &[])
.await;
assert_eq!(value, None);
client
.execute_non_query_simple(
"UPDATE enum_test SET m = 'happy' WHERE r = 'guest'::user_role",
)
.await
.unwrap();
let value: Option<Mood> = client
.read_single_value("SELECT m FROM enum_test WHERE r = 'guest'::user_role", &[])
.await;
assert_eq!(value, Some(Mood::Happy));
}
#[test]
async fn test_enum_array_text_mode() {
let mut client = setup_enum_client().await;
let value: Vec<Mood> = client
.read_single_value_simple("SELECT ARRAY['happy', 'sad', 'neutral']::mood[]")
.await;
assert_eq!(value, vec![Mood::Happy, Mood::Sad, Mood::Neutral]);
}
#[test]
async fn test_enum_array_binary_mode() {
let mut client = setup_enum_client().await;
let value: Vec<Mood> = client
.read_single_value("SELECT ARRAY['happy', 'sad']::mood[]", &[])
.await;
assert_eq!(value, vec![Mood::Happy, Mood::Sad]);
}
#[test]
async fn test_enum_empty_array() {
let mut client = setup_enum_client().await;
let value: Vec<Mood> = client
.read_single_value_simple("SELECT ARRAY[]::mood[]")
.await;
assert_eq!(value, Vec::<Mood>::new());
let value: Vec<Mood> = client
.read_single_value("SELECT ARRAY[]::mood[]", &[])
.await;
assert_eq!(value, Vec::<Mood>::new());
}
#[test]
async fn test_enum_custom_labels() {
let mut client = setup_enum_client().await;
let value: UserRole = client
.read_single_value_simple("SELECT 'regular_user'::user_role")
.await;
assert_eq!(value, UserRole::Regular);
let value: UserRole = client
.read_single_value("SELECT 'admin'::user_role", &[])
.await;
assert_eq!(value, UserRole::Admin);
}
#[test]
async fn test_pool_no_enums_no_query() {
let settings = get_settings();
let pool = PostgresPool::new(TokioConnectionFactory, settings)
.await
.unwrap();
assert!(pool.enum_registry().is_empty());
}
#[derive(Debug, Clone, PartialEq, PostgresEnum)]
#[postgres(name = "nonexistent_enum_type_12345")]
enum NonexistentEnum {
A,
}
#[test]
async fn test_unregistered_enum_silently_skipped() {
let settings = get_settings().register_enum::<NonexistentEnum>();
let pool = PostgresPool::new(TokioConnectionFactory, settings)
.await
.unwrap();
assert!(pool.enum_registry().is_empty());
}
}
}