mod common;
use common::pgwire_harness::TestServer;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn create_enum_type_and_show() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE user_status AS ENUM ('active', 'inactive', 'pending')")
.await
.expect("create enum type");
let rows = srv.query_rows("SHOW TYPES").await.expect("show types");
let names: Vec<&str> = rows.iter().map(|r| r[0].as_str()).collect();
assert!(
names.contains(&"user_status"),
"user_status must appear in SHOW TYPES: {names:?}"
);
let row = rows.iter().find(|r| r[0] == "user_status").unwrap();
assert_eq!(row[1], "enum", "kind must be 'enum'");
assert!(
row[2].contains("active"),
"definition must contain 'active': {}",
row[2]
);
assert!(
row[2].contains("inactive"),
"definition must contain 'inactive': {}",
row[2]
);
let oid: u32 = row[3].parse().expect("oid must be numeric");
assert!(
oid >= 70_000,
"OID must be in user-type range (>= 70000): {oid}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_type_removes_from_show() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral')")
.await
.expect("create");
srv.exec("DROP TYPE mood").await.expect("drop");
let rows = srv.query_rows("SHOW TYPES").await.expect("show types");
let names: Vec<&str> = rows.iter().map(|r| r[0].as_str()).collect();
assert!(
!names.contains(&"mood"),
"mood must not appear after DROP TYPE: {names:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_type_if_exists_no_error() {
let srv = TestServer::start().await;
srv.exec("DROP TYPE IF EXISTS nonexistent_type")
.await
.expect("DROP TYPE IF EXISTS on missing type must succeed");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn duplicate_create_type_error() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE color AS ENUM ('red', 'green', 'blue')")
.await
.expect("first create");
let err = srv
.exec("CREATE TYPE color AS ENUM ('cyan', 'magenta')")
.await
.expect_err("duplicate create must fail");
let msg = err.to_string();
assert!(
msg.contains("already exists") || msg.contains("42710"),
"error must mention already-exists: {msg}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn enum_column_validates_labels() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE order_state AS ENUM ('new', 'shipped', 'delivered')")
.await
.expect("create enum type");
srv.exec(
"CREATE COLLECTION orders (id TEXT PRIMARY KEY, state order_state) WITH (engine='document_strict')",
)
.await
.expect("create collection");
srv.exec("INSERT INTO orders (id, state) VALUES ('o1', 'new')")
.await
.expect("insert valid enum label");
let err = srv
.exec("INSERT INTO orders (id, state) VALUES ('o2', 'bogus')")
.await
.expect_err("insert invalid enum label must fail");
let msg = err.to_string();
assert!(
msg.contains("invalid") || msg.contains("bogus") || msg.contains("enum"),
"error must mention invalid label: {msg}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn create_composite_type_and_show() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE address AS (street TEXT, city TEXT, zip TEXT)")
.await
.expect("create composite type");
let rows = srv.query_rows("SHOW TYPES").await.expect("show types");
let row = rows
.iter()
.find(|r| r[0] == "address")
.expect("address must appear in SHOW TYPES");
assert_eq!(row[1], "composite", "kind must be 'composite'");
assert!(
row[2].contains("street"),
"definition must mention 'street': {}",
row[2]
);
assert!(
row[2].contains("city"),
"definition must mention 'city': {}",
row[2]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn alter_type_add_value_works() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE status AS ENUM ('draft', 'published')")
.await
.expect("create");
srv.exec("ALTER TYPE status ADD VALUE 'archived'")
.await
.expect("add value");
let rows = srv.query_rows("SHOW TYPES").await.expect("show types");
let row = rows
.iter()
.find(|r| r[0] == "status")
.expect("status must appear");
assert!(
row[2].contains("archived"),
"definition must contain 'archived' after ADD VALUE: {}",
row[2]
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn alter_type_add_duplicate_value_error() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE phase AS ENUM ('alpha', 'beta')")
.await
.expect("create");
let err = srv
.exec("ALTER TYPE phase ADD VALUE 'alpha'")
.await
.expect_err("duplicate ADD VALUE must fail");
let msg = err.to_string();
assert!(
msg.contains("already exists") || msg.contains("42710"),
"error must mention already-exists: {msg}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_type_blocked_when_referenced() {
let srv = TestServer::start().await;
srv.exec("CREATE TYPE priority AS ENUM ('low', 'medium', 'high')")
.await
.expect("create type");
srv.exec("CREATE COLLECTION tasks (id TEXT, prio priority) WITH (engine='document_strict')")
.await
.expect("create collection referencing type");
let err = srv
.exec("DROP TYPE priority")
.await
.expect_err("DROP TYPE must fail when referenced by a collection");
let msg = err.to_string();
assert!(
msg.contains("referenced") || msg.contains("tasks") || msg.contains("2BP01"),
"error must mention referencing collection: {msg}"
);
}
#[test]
fn catalog_custom_type_roundtrip() {
use nodedb::control::security::catalog::{CustomTypeDef, StoredCustomType, SystemCatalog};
let dir = tempfile::tempdir().unwrap();
let catalog = SystemCatalog::open(&dir.path().join("system.redb")).unwrap();
let def = StoredCustomType {
tenant_id: 1,
name: "emotion".to_string(),
def: CustomTypeDef::Enum {
labels: vec!["joy".into(), "anger".into(), "fear".into()],
},
oid: 70_001,
created_at: 42,
};
catalog.put_custom_type(&def).unwrap();
drop(catalog);
let catalog2 = SystemCatalog::open(&dir.path().join("system.redb")).unwrap();
let loaded = catalog2.get_custom_type(1, "emotion").unwrap().unwrap();
assert_eq!(loaded.name, "emotion");
assert_eq!(loaded.oid, 70_001);
match &loaded.def {
CustomTypeDef::Enum { labels } => {
assert_eq!(labels, &vec!["joy", "anger", "fear"]);
}
other => panic!("expected Enum, got {other:?}"),
}
assert!(catalog2.delete_custom_type(1, "emotion").unwrap());
assert!(catalog2.get_custom_type(1, "emotion").unwrap().is_none());
}