use dibs::diff::SchemaExt;
use dibs::introspect::SchemaIntrospect;
use dibs::schema::{SchemaCodegen, collect_schema};
use dibs::{PgType, Schema, Table};
use dockside::{Container, containers};
use facet::Facet;
use std::time::Duration;
use tokio_postgres::NoTls;
#[derive(Facet)]
#[facet(derive(dibs::Table), dibs::table = "test_users")]
struct TestUser {
#[facet(dibs::pk)]
id: i64,
#[facet(dibs::unique)]
email: String,
#[facet(dibs::index)]
name: String,
bio: Option<String>,
#[facet(dibs::default = "0")]
created_at: i64,
}
#[derive(Facet)]
#[facet(derive(dibs::Table), dibs::table = "test_posts")]
struct TestPost {
#[facet(dibs::pk)]
id: i64,
title: String,
body: String,
#[facet(dibs::fk = "test_users.id", dibs::index)]
author_id: i64,
}
fn test_column(
name: &str,
pg_type: dibs::PgType,
nullable: bool,
primary_key: bool,
unique: bool,
) -> dibs::Column {
dibs::Column {
name: name.to_string(),
pg_type,
rust_type: None,
nullable,
default: None,
primary_key,
unique,
auto_generated: false,
long: false,
label: false,
enum_variants: vec![],
doc: None,
lang: None,
icon: None,
subtype: None,
}
}
fn test_column_with_default(name: &str, pg_type: dibs::PgType, default: &str) -> dibs::Column {
dibs::Column {
name: name.to_string(),
pg_type,
rust_type: None,
nullable: false,
default: Some(default.to_string()),
primary_key: false,
unique: false,
auto_generated: false,
long: false,
label: false,
enum_variants: vec![],
doc: None,
lang: None,
icon: None,
subtype: None,
}
}
fn test_table(
name: &str,
columns: Vec<dibs::Column>,
foreign_keys: Vec<dibs::ForeignKey>,
indices: Vec<dibs::Index>,
) -> dibs::Table {
dibs::Table {
name: name.to_string(),
columns,
check_constraints: Vec::new(),
trigger_checks: Vec::new(),
foreign_keys,
indices,
source: dibs::SourceLocation {
file: None,
line: None,
column: None,
},
doc: None,
icon: None,
}
}
fn make_schema(tables: Vec<dibs::Table>) -> Schema {
Schema {
tables: tables.into_iter().map(|t| (t.name.clone(), t)).collect(),
}
}
async fn create_postgres_container() -> (Container, tokio_postgres::Client) {
let container = tokio::task::spawn_blocking(|| {
let container = Container::run(containers::postgres("18", "postgres"))
.expect("Failed to start Postgres container");
container
.wait_for_log(
"database system is ready to accept connections",
Duration::from_secs(30),
)
.expect("Postgres failed to become ready");
let port = container
.wait_for_port(5432, Duration::from_secs(5))
.expect("Failed to connect to postgres port");
(container, port)
})
.await
.expect("spawn_blocking failed");
let (container, port) = container;
let connection_string = format!(
"host=127.0.0.1 port={} user=postgres password=postgres dbname=postgres",
port
);
let mut last_err = None;
let mut client_and_conn = None;
for _ in 0..30 {
match tokio_postgres::connect(&connection_string, NoTls).await {
Ok(c) => {
client_and_conn = Some(c);
break;
}
Err(e) => {
last_err = Some(e);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
let (client, connection) = client_and_conn
.ok_or_else(|| last_err.unwrap())
.expect("Failed to connect to Postgres after retries");
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {}", e);
}
});
(container, client)
}
#[tokio::test]
async fn test_schema_collection() {
let schema = collect_schema();
let table_names: Vec<_> = schema.tables.values().map(|t| t.name.as_str()).collect();
assert!(
table_names.contains(&"test_users"),
"Expected test_users table, got: {:?}",
table_names
);
assert!(
table_names.contains(&"test_posts"),
"Expected test_posts table, got: {:?}",
table_names
);
}
#[tokio::test]
async fn test_sql_generation() {
let schema = collect_schema();
let sql = schema.to_sql();
assert!(sql.contains("CREATE TABLE \"test_users\""));
assert!(sql.contains("CREATE TABLE \"test_posts\""));
assert!(sql.contains("PRIMARY KEY"));
assert!(sql.contains("NOT NULL"));
assert!(sql.contains("UNIQUE"));
assert!(sql.contains("FOREIGN KEY"));
assert!(sql.contains("CREATE INDEX"));
}
#[tokio::test]
async fn test_auto_generated_identity_column() {
let (_container, client) = create_postgres_container().await;
let mut id = test_column("id", PgType::BigInt, false, true, false);
id.auto_generated = true;
let label = test_column("label", PgType::Text, false, false, false);
let widgets = test_table("test_widgets", vec![id, label], vec![], vec![]);
let ddl = dibs::schema::create_table_sql(&widgets);
assert!(
ddl.contains("GENERATED BY DEFAULT AS IDENTITY"),
"auto-generated integer column should be an identity column. Got:\n{ddl}"
);
client.batch_execute(&ddl).await.expect("create table");
let row = client
.query_one(
"INSERT INTO test_widgets (label) VALUES ($1) RETURNING id",
&[&"first"],
)
.await
.expect("insert without id should succeed");
let first_id: i64 = row.get(0);
assert!(
first_id >= 1,
"identity should assign a value, got {first_id}"
);
let db_schema = Schema::from_database(&client).await.expect("introspect");
let desired = make_schema(vec![widgets]);
let diff = desired.diff(&db_schema);
assert!(
diff.is_empty(),
"identity column should round-trip with no diff. Got: {diff}"
);
}
#[tokio::test]
async fn test_execute_schema_on_postgres() {
let (_container, client) = create_postgres_container().await;
let schema = collect_schema();
let test_tables: Vec<&Table> = schema
.tables
.values()
.filter(|t| t.name.starts_with("test_"))
.collect();
for table in &test_tables {
let sql = dibs::schema::create_table_sql(table);
client
.batch_execute(&sql)
.await
.unwrap_or_else(|e| panic!("Failed to create table {}: {e}", table.name));
}
for table in &test_tables {
for fk in &table.foreign_keys {
let sql = format!(
"ALTER TABLE {} ADD CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {}({})",
table.name,
table.name,
fk.columns.join("_"),
fk.columns.join(", "),
fk.references_table,
fk.references_columns.join(", ")
);
client
.batch_execute(&sql)
.await
.unwrap_or_else(|e| panic!("Failed to add FK on {}: {e}", table.name));
}
}
for table in &test_tables {
for idx in &table.indices {
let sql = dibs::schema::create_index_sql(table, idx);
client
.batch_execute(&sql)
.await
.unwrap_or_else(|e| panic!("Failed to create index {}: {e}", idx.name));
}
}
let rows = client
.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name",
&[],
)
.await
.expect("Failed to query tables");
let table_names: Vec<String> = rows.iter().map(|r| r.get(0)).collect();
assert!(table_names.contains(&"test_users".to_string()));
assert!(table_names.contains(&"test_posts".to_string()));
let rows = client
.query(
"SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'test_users' ORDER BY ordinal_position",
&[],
)
.await
.expect("Failed to query columns");
let columns: Vec<(String, String, String)> = rows
.iter()
.map(|r| (r.get(0), r.get(1), r.get(2)))
.collect();
assert!(columns.iter().any(|(name, _, _)| name == "id"));
assert!(columns.iter().any(|(name, _, _)| name == "email"));
assert!(
columns
.iter()
.any(|(name, _, nullable)| name == "bio" && nullable == "YES")
);
let rows = client
.query(
"SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = 'test_posts' AND constraint_type = 'FOREIGN KEY'",
&[],
)
.await
.expect("Failed to query constraints");
assert!(
!rows.is_empty(),
"Expected foreign key constraint on test_posts"
);
let rows = client
.query(
"SELECT indexname FROM pg_indexes WHERE tablename = 'test_users' AND indexname LIKE 'idx_%'",
&[],
)
.await
.expect("Failed to query indexes");
assert!(!rows.is_empty(), "Expected index on test_users");
}
#[tokio::test]
async fn test_insert_and_query_data() {
let (_container, client) = create_postgres_container().await;
let schema = collect_schema();
let test_tables: Vec<&Table> = schema
.tables
.values()
.filter(|t| t.name.starts_with("test_"))
.collect();
for table in &test_tables {
client
.batch_execute(&dibs::schema::create_table_sql(table))
.await
.unwrap();
}
for table in &test_tables {
for fk in &table.foreign_keys {
let sql = format!(
"ALTER TABLE {} ADD CONSTRAINT fk_{}_{} FOREIGN KEY ({}) REFERENCES {}({})",
table.name,
table.name,
fk.columns.join("_"),
fk.columns.join(", "),
fk.references_table,
fk.references_columns.join(", ")
);
client.batch_execute(&sql).await.unwrap();
}
}
client
.execute(
"INSERT INTO test_users (id, email, name, bio, created_at) VALUES ($1, $2, $3, $4, $5)",
&[
&1i64,
&"alice@example.com",
&"Alice",
&Some("Hello!"),
&1234567890i64,
],
)
.await
.expect("Failed to insert user");
client
.execute(
"INSERT INTO test_posts (id, title, body, author_id) VALUES ($1, $2, $3, $4)",
&[&1i64, &"First Post", &"Hello World", &1i64],
)
.await
.expect("Failed to insert post");
let rows = client
.query(
"SELECT id, email, name FROM test_users WHERE id = $1",
&[&1i64],
)
.await
.expect("Failed to query user");
assert_eq!(rows.len(), 1);
let row = &rows[0];
let email: &str = row.get(1);
let name: &str = row.get(2);
assert_eq!(email, "alice@example.com");
assert_eq!(name, "Alice");
let rows = client
.query(
"SELECT p.title, u.name FROM test_posts p JOIN test_users u ON p.author_id = u.id",
&[],
)
.await
.expect("Failed to query with join");
assert_eq!(rows.len(), 1);
let title: &str = rows[0].get(0);
let author: &str = rows[0].get(1);
assert_eq!(title, "First Post");
assert_eq!(author, "Alice");
}
#[tokio::test]
async fn test_diff_rust_vs_database() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE test_users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at BIGINT NOT NULL DEFAULT 0,
legacy_field TEXT
);
CREATE INDEX idx_test_users_name ON test_users(name);
"#,
)
.await
.expect("Failed to create tables");
let db_schema = Schema::from_database(&client)
.await
.expect("Failed to introspect schema");
let rust_schema = collect_schema();
let rust_test_users = rust_schema
.tables
.values()
.find(|t| t.name == "test_users")
.expect("test_users should be in Rust schema");
let rust_schema_subset = make_schema(vec![rust_test_users.clone()]);
let diff = rust_schema_subset.diff(&db_schema);
assert!(!diff.is_empty(), "Should detect differences");
let users_diff = diff
.table_diffs
.iter()
.find(|d| d.table == "test_users")
.expect("Should have diff for test_users");
let has_rename = users_diff
.changes
.iter()
.any(|c| matches!(c, dibs::Change::RenameColumn { from, to } if from == "legacy_field" && to == "bio"));
assert!(
has_rename,
"Should detect rename from 'legacy_field' to 'bio'. Got: {}",
diff
);
}
#[tokio::test]
async fn test_diff_no_changes() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE test_users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
bio TEXT,
created_at BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX idx_test_users_name ON test_users(name);
"#,
)
.await
.expect("Failed to create tables");
let db_schema = Schema::from_database(&client)
.await
.expect("Failed to introspect schema");
let rust_schema = collect_schema();
let rust_test_users = rust_schema
.tables
.values()
.find(|t| t.name == "test_users")
.expect("test_users should be in Rust schema");
let rust_schema_subset = make_schema(vec![rust_test_users.clone()]);
let diff = rust_schema_subset.diff(&db_schema);
assert!(
diff.is_empty(),
"Should detect no changes when schemas match. Got: {}",
diff
);
}
#[tokio::test]
async fn test_introspect_schema_from_database() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
bio TEXT,
created_at BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE posts (
id BIGINT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL,
author_id BIGINT NOT NULL REFERENCES users(id)
);
CREATE INDEX idx_posts_author_id ON posts(author_id);
CREATE INDEX idx_users_name ON users(name);
"#,
)
.await
.expect("Failed to create tables");
let schema = Schema::from_database(&client)
.await
.expect("Failed to introspect schema");
assert_eq!(schema.tables.len(), 2);
let users = schema.tables.values().find(|t| t.name == "users").unwrap();
assert_eq!(users.columns.len(), 5);
let id_col = users.columns.iter().find(|c| c.name == "id").unwrap();
assert!(id_col.primary_key);
assert_eq!(id_col.pg_type, dibs::PgType::BigInt);
assert!(!id_col.nullable);
let email_col = users.columns.iter().find(|c| c.name == "email").unwrap();
assert!(email_col.unique);
assert!(!email_col.nullable);
let bio_col = users.columns.iter().find(|c| c.name == "bio").unwrap();
assert!(bio_col.nullable);
assert!(!bio_col.unique);
assert!(users.indices.iter().any(|i| i.name == "idx_users_name"));
let posts = schema.tables.values().find(|t| t.name == "posts").unwrap();
assert_eq!(posts.foreign_keys.len(), 1);
let fk = &posts.foreign_keys[0];
assert_eq!(fk.references_table, "users");
assert_eq!(fk.references_columns, vec!["id"]);
assert!(
posts
.indices
.iter()
.any(|i| i.name == "idx_posts_author_id")
);
}
#[tokio::test]
async fn test_table_rename_execution() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE users (
id BIGINT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL
);
CREATE TABLE posts (
id BIGINT PRIMARY KEY,
title TEXT NOT NULL,
author_id BIGINT NOT NULL REFERENCES users(id)
);
CREATE INDEX idx_posts_author_id ON posts(author_id);
"#,
)
.await
.expect("Failed to create initial schema");
let db_schema = Schema::from_database(&client)
.await
.expect("Failed to introspect");
let desired = make_schema(vec![
test_table(
"user",
vec![
test_column("id", dibs::PgType::BigInt, false, true, false),
test_column("email", dibs::PgType::Text, false, false, true),
test_column("name", dibs::PgType::Text, false, false, false),
],
vec![],
vec![],
),
test_table(
"post",
vec![
test_column("id", dibs::PgType::BigInt, false, true, false),
test_column("title", dibs::PgType::Text, false, false, false),
test_column("author_id", dibs::PgType::BigInt, false, false, false),
],
vec![dibs::ForeignKey {
columns: vec!["author_id".to_string()],
references_table: "user".to_string(),
references_columns: vec!["id".to_string()],
}],
vec![dibs::Index {
name: "idx_post_author_id".to_string(),
columns: vec![dibs::IndexColumn::new("author_id")],
unique: false,
where_clause: None,
}],
),
]);
let diff = desired.diff(&db_schema);
println!("Diff:\n{}", diff);
assert!(
diff.table_diffs.iter().any(|td| td.changes.iter().any(|c| {
matches!(c, dibs::Change::RenameTable { from, to } if from == "users" && to == "user")
})),
"Should detect users -> user rename"
);
assert!(
diff.table_diffs.iter().any(|td| td.changes.iter().any(|c| {
matches!(c, dibs::Change::RenameTable { from, to } if from == "posts" && to == "post")
})),
"Should detect posts -> post rename"
);
let sql = diff.to_sql();
println!("Migration SQL:\n{}", sql);
client
.batch_execute(&sql)
.await
.expect("Failed to execute rename migration");
let rows = client
.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name",
&[],
)
.await
.expect("Failed to query tables");
let table_names: Vec<String> = rows.iter().map(|r| r.get(0)).collect();
assert!(
table_names.contains(&"user".to_string()),
"Expected 'user' table, got: {:?}",
table_names
);
assert!(
table_names.contains(&"post".to_string()),
"Expected 'post' table, got: {:?}",
table_names
);
assert!(
!table_names.contains(&"users".to_string()),
"Old 'users' table should not exist"
);
assert!(
!table_names.contains(&"posts".to_string()),
"Old 'posts' table should not exist"
);
client
.execute(
"INSERT INTO \"user\" (id, email, name) VALUES ($1, $2, $3)",
&[&1i64, &"alice@example.com", &"Alice"],
)
.await
.expect("Failed to insert user");
client
.execute(
"INSERT INTO post (id, title, author_id) VALUES ($1, $2, $3)",
&[&1i64, &"Hello World", &1i64],
)
.await
.expect("Failed to insert post");
let result = client
.execute(
"INSERT INTO post (id, title, author_id) VALUES ($1, $2, $3)",
&[&2i64, &"Bad Post", &999i64], )
.await;
assert!(
result.is_err(),
"FK constraint should reject invalid author_id"
);
}
#[tokio::test]
async fn test_column_type_change() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL
);
"#,
)
.await
.expect("Failed to create table");
client
.execute(
"INSERT INTO products (id, name, price) VALUES ($1, $2, $3)",
&[&1i64, &"Widget", &100i32],
)
.await
.expect("Failed to insert");
let db_schema = Schema::from_database(&client)
.await
.expect("Failed to introspect");
let desired = make_schema(vec![test_table(
"products",
vec![
test_column("id", dibs::PgType::BigInt, false, true, false),
test_column("name", dibs::PgType::Text, false, false, false),
test_column("price", dibs::PgType::BigInt, false, false, false), ],
vec![],
vec![],
)]);
let diff = desired.diff(&db_schema);
println!("Diff:\n{}", diff);
assert!(
diff.table_diffs.iter().any(|td| td.changes.iter().any(|c| {
matches!(c, dibs::Change::AlterColumnType { name, .. } if name == "price")
})),
"Should detect price type change"
);
let sql = diff.to_sql();
println!("Migration SQL:\n{}", sql);
client
.batch_execute(&sql)
.await
.expect("Failed to execute type change migration");
let rows = client
.query("SELECT price FROM products WHERE id = $1", &[&1i64])
.await
.expect("Failed to query");
let price: i64 = rows[0].get(0);
assert_eq!(price, 100, "Data should be preserved after type change");
client
.execute(
"INSERT INTO products (id, name, price) VALUES ($1, $2, $3)",
&[&2i64, &"Big Widget", &10_000_000_000i64],
)
.await
.expect("BIGINT should accept large values");
}
#[tokio::test]
async fn test_add_column_with_default() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE items (
id BIGINT PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO items (id, name) VALUES (1, 'First');
"#,
)
.await
.expect("Failed to create table");
let db_schema = Schema::from_database(&client)
.await
.expect("Failed to introspect");
let desired = make_schema(vec![test_table(
"items",
vec![
test_column("id", dibs::PgType::BigInt, false, true, false),
test_column("name", dibs::PgType::Text, false, false, false),
test_column_with_default("quantity", dibs::PgType::Integer, "0"),
],
vec![],
vec![],
)]);
let diff = desired.diff(&db_schema);
println!("Diff:\n{}", diff);
let sql = diff.to_sql();
println!("Migration SQL:\n{}", sql);
client
.batch_execute(&sql)
.await
.expect("Failed to execute add column migration");
let rows = client
.query("SELECT quantity FROM items WHERE id = $1", &[&1i64])
.await
.expect("Failed to query");
let quantity: i32 = rows[0].get(0);
assert_eq!(quantity, 0, "Existing row should have default value");
}
#[tokio::test]
async fn test_meta_tables() {
let (_container, client) = create_postgres_container().await;
let meta_sql = dibs::create_meta_tables_sql();
client
.batch_execute(&meta_sql)
.await
.expect("Failed to create meta tables");
let rows = client
.query(
"SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE '__dibs_%' ORDER BY table_name",
&[],
)
.await
.expect("Failed to query meta tables");
let table_names: Vec<String> = rows.iter().map(|r| r.get(0)).collect();
assert!(table_names.contains(&"__dibs_migrations".to_string()));
assert!(table_names.contains(&"__dibs_tables".to_string()));
assert!(table_names.contains(&"__dibs_columns".to_string()));
assert!(table_names.contains(&"__dibs_indices".to_string()));
let schema = collect_schema();
let sync_sql = dibs::sync_tables_sql(&schema, Some("test_migration"));
client
.batch_execute(&sync_sql)
.await
.expect("Failed to sync meta tables");
let rows = client
.query(
"SELECT table_name, source_file, doc_comment, created_by_migration FROM __dibs_tables ORDER BY table_name",
&[],
)
.await
.expect("Failed to query __dibs_tables");
assert!(!rows.is_empty(), "Expected rows in __dibs_tables");
let test_user_row = rows.iter().find(|r| {
let name: &str = r.get(0);
name == "test_users"
});
assert!(
test_user_row.is_some(),
"Expected test_users in __dibs_tables"
);
let row = test_user_row.unwrap();
let source_file: Option<&str> = row.get(1);
let migration: Option<&str> = row.get(3);
assert!(
source_file.is_some(),
"Expected source_file to be populated"
);
assert!(source_file.unwrap().contains("postgres_integration.rs"));
assert_eq!(migration, Some("test_migration"));
let rows = client
.query(
"SELECT column_name, sql_type, is_primary_key, is_unique FROM __dibs_columns WHERE table_name = 'test_users' ORDER BY column_name",
&[],
)
.await
.expect("Failed to query __dibs_columns");
assert!(!rows.is_empty(), "Expected rows in __dibs_columns");
let id_row = rows.iter().find(|r| {
let name: &str = r.get(0);
name == "id"
});
assert!(id_row.is_some());
let is_pk: bool = id_row.unwrap().get(2);
assert!(is_pk, "Expected id to be primary key");
let email_row = rows.iter().find(|r| {
let name: &str = r.get(0);
name == "email"
});
assert!(email_row.is_some());
let is_unique: bool = email_row.unwrap().get(3);
assert!(is_unique, "Expected email to be unique");
let record_sql = dibs::record_migration_sql("test_migration", Some("abc123"), Some(42));
client
.batch_execute(&record_sql)
.await
.expect("Failed to record migration");
let rows = client
.query(
"SELECT name, checksum, execution_time_ms FROM __dibs_migrations WHERE name = 'test_migration'",
&[],
)
.await
.expect("Failed to query __dibs_migrations");
assert_eq!(rows.len(), 1);
let checksum: Option<&str> = rows[0].get(1);
let time_ms: Option<i32> = rows[0].get(2);
assert_eq!(checksum, Some("abc123"));
assert_eq!(time_ms, Some(42));
}
#[tokio::test]
async fn test_error_message_text_vs_bigint() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE error_test_1 (
id BIGINT PRIMARY KEY,
value TEXT NOT NULL
);
INSERT INTO error_test_1 (id, value) VALUES (1, 'hello');
"#,
)
.await
.expect("Failed to create table");
let columns = vec![
("id".to_string(), PgType::BigInt),
("value".to_string(), PgType::BigInt), ];
let row = client
.query_one("SELECT id, value FROM error_test_1 WHERE id = 1", &[])
.await
.expect("Query failed");
let ctx = dibs::query::RowContext {
table_name: "error_test_1",
};
let result = dibs::query::pg_row_to_row(&row, &columns, &ctx);
assert!(result.is_err());
let error = result.unwrap_err();
insta::assert_snapshot!("text_vs_bigint", error.to_string());
}
#[tokio::test]
async fn test_error_message_integer_vs_text() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE error_test_2 (
id BIGINT PRIMARY KEY,
count INTEGER NOT NULL
);
INSERT INTO error_test_2 (id, count) VALUES (1, 42);
"#,
)
.await
.expect("Failed to create table");
let columns = vec![
("id".to_string(), PgType::BigInt),
("count".to_string(), PgType::Text), ];
let row = client
.query_one("SELECT id, count FROM error_test_2 WHERE id = 1", &[])
.await
.expect("Query failed");
let ctx = dibs::query::RowContext {
table_name: "error_test_2",
};
let result = dibs::query::pg_row_to_row(&row, &columns, &ctx);
assert!(result.is_err());
let error = result.unwrap_err();
insta::assert_snapshot!("integer_vs_text", error.to_string());
}
#[tokio::test]
async fn test_error_message_jsonb_vs_text() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE error_test_3 (
id BIGINT PRIMARY KEY,
data JSONB NOT NULL
);
INSERT INTO error_test_3 (id, data) VALUES (1, '{"key": "value"}');
"#,
)
.await
.expect("Failed to create table");
let columns = vec![
("id".to_string(), PgType::BigInt),
("data".to_string(), PgType::Text), ];
let row = client
.query_one("SELECT id, data FROM error_test_3 WHERE id = 1", &[])
.await
.expect("Query failed");
let ctx = dibs::query::RowContext {
table_name: "error_test_3",
};
let result = dibs::query::pg_row_to_row(&row, &columns, &ctx);
assert!(result.is_err());
let error = result.unwrap_err();
insta::assert_snapshot!("jsonb_vs_text", error.to_string());
}
#[tokio::test]
async fn test_error_message_text_vs_jsonb() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE error_test_4 (
id BIGINT PRIMARY KEY,
attributes TEXT
);
INSERT INTO error_test_4 (id, attributes) VALUES (1, 'not json');
"#,
)
.await
.expect("Failed to create table");
let columns = vec![
("id".to_string(), PgType::BigInt),
("attributes".to_string(), PgType::Jsonb), ];
let row = client
.query_one("SELECT id, attributes FROM error_test_4 WHERE id = 1", &[])
.await
.expect("Query failed");
let ctx = dibs::query::RowContext {
table_name: "error_test_4",
};
let result = dibs::query::pg_row_to_row(&row, &columns, &ctx);
assert!(result.is_err());
let error = result.unwrap_err();
insta::assert_snapshot!("text_vs_jsonb", error.to_string());
}
#[tokio::test]
async fn test_error_message_boolean_vs_integer() {
let (_container, client) = create_postgres_container().await;
client
.batch_execute(
r#"
CREATE TABLE error_test_5 (
id BIGINT PRIMARY KEY,
is_active BOOLEAN NOT NULL
);
INSERT INTO error_test_5 (id, is_active) VALUES (1, true);
"#,
)
.await
.expect("Failed to create table");
let columns = vec![
("id".to_string(), PgType::BigInt),
("is_active".to_string(), PgType::Integer), ];
let row = client
.query_one("SELECT id, is_active FROM error_test_5 WHERE id = 1", &[])
.await
.expect("Query failed");
let ctx = dibs::query::RowContext {
table_name: "error_test_5",
};
let result = dibs::query::pg_row_to_row(&row, &columns, &ctx);
assert!(result.is_err());
let error = result.unwrap_err();
insta::assert_snapshot!("boolean_vs_integer", error.to_string());
}