use umbral::orm::{ArrayElement, Model, SqlType};
#[derive(Debug, Clone, sqlx::FromRow, umbral::orm::Model)]
#[umbral(table = "umbral_phase41_event")]
pub struct Event {
pub id: i64,
pub kind: String,
pub tags: Vec<String>,
pub scores: Option<Vec<i64>>,
}
#[test]
fn derive_classifies_vec_as_array_sqltype() {
let by_name: std::collections::HashMap<&str, &umbral::orm::FieldSpec> = <Event as Model>::FIELDS
.iter()
.map(|f| (f.name, f))
.collect();
let tags = by_name.get("tags").expect("tags field");
assert_eq!(tags.ty, SqlType::Array(ArrayElement::Text));
assert!(!tags.nullable, "Vec<T> by itself is non-nullable");
let scores = by_name.get("scores").expect("scores field");
assert_eq!(scores.ty, SqlType::Array(ArrayElement::BigInt));
assert!(scores.nullable, "Option<Vec<T>> is the nullable variant");
}
#[test]
fn array_element_round_trips_through_to_sql_type() {
assert_eq!(ArrayElement::SmallInt.to_sql_type(), SqlType::SmallInt);
assert_eq!(ArrayElement::Integer.to_sql_type(), SqlType::Integer);
assert_eq!(ArrayElement::BigInt.to_sql_type(), SqlType::BigInt);
assert_eq!(ArrayElement::Real.to_sql_type(), SqlType::Real);
assert_eq!(ArrayElement::Double.to_sql_type(), SqlType::Double);
assert_eq!(ArrayElement::Boolean.to_sql_type(), SqlType::Boolean);
assert_eq!(ArrayElement::Text.to_sql_type(), SqlType::Text);
assert_eq!(ArrayElement::Uuid.to_sql_type(), SqlType::Uuid);
}
#[test]
fn postgres_ddl_renders_array_suffix() {
use umbral::migrate::{Column, Operation, render_operation_for};
let op = Operation::CreateTable {
table: "umbral_phase41_event".to_string(),
columns: vec![
Column {
name: "id".to_string(),
ty: SqlType::BigInt,
primary_key: true,
nullable: false,
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: Vec::new(),
choice_labels: Vec::new(),
default: String::new(),
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: String::new(),
example: String::new(),
widget: None,
supported_backends: Vec::new(),
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
},
Column {
name: "tags".to_string(),
ty: SqlType::Array(ArrayElement::Text),
primary_key: false,
nullable: false,
fk_target: None,
noform: false,
db_constraint: true,
noedit: false,
is_string_repr: false,
max_length: 0,
choices: Vec::new(),
choice_labels: Vec::new(),
default: String::new(),
is_multichoice: false,
unique: false,
on_delete: umbral_core::orm::FkAction::NoAction,
on_update: umbral_core::orm::FkAction::NoAction,
index: false,
auto_now_add: false,
auto_now: false,
help: String::new(),
example: String::new(),
widget: None,
supported_backends: Vec::new(),
min: None,
max: None,
text_format: ::core::option::Option::None,
slug_from: ::core::option::Option::None,
},
],
unique_together: Vec::new(),
indexes: Vec::new(),
};
let stmts = render_operation_for(&op, "postgres");
assert_eq!(stmts.len(), 1);
let sql = &stmts[0];
assert!(
sql.contains("[]"),
"Postgres Array should render with `[]` suffix; got {sql}"
);
let lower = sql.to_ascii_lowercase();
assert!(
lower.contains("text[]") || lower.contains("text []") || lower.contains("text []"),
"expected `text[]` for Vec<String> column; got {sql}"
);
}
#[tokio::test]
#[ignore = "pollutes the process-wide model registry; run isolated"]
async fn field_backend_rejects_array_on_sqlite() {
use umbral::{App, Settings};
use umbral_core::app::BuildError;
let mut settings = Settings::from_env().expect("figment defaults load");
settings.database_url = "sqlite::memory:".to_string();
let sqlite_pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
let result = App::builder()
.settings(settings)
.database("default", sqlite_pool)
.model::<Event>()
.build();
match result {
Err(BuildError::SystemCheckFailed { findings }) => {
let has = findings.iter().any(|f| f.check_id == "field.backend");
assert!(
has,
"expected a field.backend finding; got {:?}",
findings.iter().map(|f| f.check_id).collect::<Vec<_>>(),
);
}
Err(other) => panic!("expected SystemCheckFailed, got {other:?}"),
Ok(_) => panic!("expected build to fail; SQLite + Vec<i64> should be rejected"),
}
}
#[test]
fn column_const_module_has_array_types() {
use umbral::orm::column::{ArrayCol, NullableArrayCol};
let _: ArrayCol<Event> = event::TAGS;
let _: NullableArrayCol<Event> = event::SCORES;
}
#[tokio::test]
#[ignore = "needs UMBRAL_TEST_POSTGRES_URL pointing at a Postgres server"]
async fn array_field_round_trips_through_postgres() {
let url =
std::env::var("UMBRAL_TEST_POSTGRES_URL").expect("UMBRAL_TEST_POSTGRES_URL must be set");
let pool = sqlx::PgPool::connect(&url).await.unwrap();
sqlx::query("DROP TABLE IF EXISTS umbral_phase41_event")
.execute(&pool)
.await
.unwrap();
sqlx::query(
"CREATE TABLE umbral_phase41_event ( \
id BIGSERIAL PRIMARY KEY, \
kind TEXT NOT NULL, \
tags TEXT[] NOT NULL, \
scores BIGINT[] \
)",
)
.execute(&pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO umbral_phase41_event (kind, tags, scores) VALUES ($1, $2, $3), ($4, $5, NULL)",
)
.bind("startup")
.bind(vec!["info".to_string(), "boot".to_string()])
.bind(vec![10i64, 20, 30])
.bind("draft")
.bind(vec!["wip".to_string()])
.execute(&pool)
.await
.unwrap();
let mut rows = Event::objects().fetch_pg(&pool).await.unwrap();
rows.sort_by_key(|r| r.id);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].kind, "startup");
assert_eq!(rows[0].tags, vec!["info".to_string(), "boot".to_string()]);
assert_eq!(rows[0].scores, Some(vec![10, 20, 30]));
assert_eq!(rows[1].kind, "draft");
assert_eq!(rows[1].tags, vec!["wip".to_string()]);
assert!(rows[1].scores.is_none());
}
#[derive(Debug, sqlx::FromRow, serde::Serialize, serde::Deserialize, umbral::orm::Model)]
#[umbral(table = "backend_gate_doc")]
struct GatedDoc {
id: i64,
title: String,
#[umbral(backend = "postgres")]
metadata: String,
}
#[tokio::test]
#[ignore = "pollutes the process-wide model registry; run isolated"]
async fn field_backend_rejects_declared_postgres_only_on_sqlite() {
use umbral::{App, Settings};
use umbral_core::app::BuildError;
let mut settings = Settings::from_env().expect("figment defaults load");
settings.database_url = "sqlite::memory:".to_string();
let sqlite_pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap();
let result = App::builder()
.settings(settings)
.database("default", sqlite_pool)
.model::<GatedDoc>()
.build();
match result {
Err(BuildError::SystemCheckFailed { findings }) => {
let has = findings.iter().any(|f| f.check_id == "field.backend");
assert!(
has,
"expected a field.backend finding for the declared gate; got {:?}",
findings.iter().map(|f| f.check_id).collect::<Vec<_>>(),
);
let f = findings
.iter()
.find(|f| f.check_id == "field.backend")
.unwrap();
assert!(
f.message.contains("metadata"),
"the finding message should mention the gated field; got: {}",
f.message,
);
}
Err(other) => panic!("expected SystemCheckFailed, got {other:?}"),
Ok(_) => panic!(
"expected build to fail; declared backend = postgres should be rejected on SQLite"
),
}
}