use sabiql_app::ports::outbound::{AccessMode, DbOperationError, MetadataProvider, QueryExecutor};
use sabiql_infra::adapters::PostgresAdapter;
use crate::tests::harness::postgres::{
postgres_bad_dsn, postgres_integration_dsn, with_postgres_test_db,
};
mod metadata_fetch {
use super::*;
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn fetch_metadata_returns_schemas() {
with_postgres_test_db(|db| {
Box::pin(async move {
let metadata = db
.adapter()
.fetch_metadata(db.dsn())
.await
.map_err(|err| err.to_string())?;
if !metadata.schemas.iter().any(|s| s.name == db.schema()) {
return Err(format!(
"expected fixture schema '{}' in metadata",
db.schema()
));
}
if !metadata
.table_summaries
.iter()
.any(|table| table.schema == db.schema() && table.name == db.table())
{
return Err(format!(
"expected fixture table '{}.{}' in metadata",
db.schema(),
db.table()
));
}
Ok(())
})
})
.await;
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn fetch_table_detail_returns_columns() {
with_postgres_test_db(|db| {
Box::pin(async move {
let detail = db
.adapter()
.fetch_table_detail(db.dsn(), db.schema(), db.table())
.await
.map_err(|err| err.to_string())?;
if detail.columns.is_empty() {
return Err("expected at least one column for fixture table".to_string());
}
Ok(())
})
})
.await;
}
}
mod query_execution {
use super::*;
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn execute_preview_returns_columns() {
with_postgres_test_db(|db| {
Box::pin(async move {
let result = db
.adapter()
.execute_preview(db.dsn(), db.schema(), db.table(), 10, 0)
.await
.map_err(|err| err.to_string())?;
if result.columns.is_empty() {
return Err("expected columns for fixture table preview".to_string());
}
Ok(())
})
})
.await;
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn execute_adhoc_select_returns_query_result() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let result = adapter
.execute_adhoc(&dsn, "SELECT 1 AS value", AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.columns, vec!["value"]);
assert_eq!(result.data_row_count(), 1);
assert_eq!(result.display_row_at(0), Some(vec!["1".to_string()]));
}
}
mod multi_statement_boundaries {
use super::*;
use sabiql_domain::CommandTag;
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn returns_last_result_set() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let result = adapter
.execute_adhoc(
&dsn,
"SELECT 1 AS a; SELECT 2 AS b, 3 AS c",
AccessMode::ReadWrite,
)
.await
.unwrap();
assert_eq!(result.columns, vec!["b", "c"]);
assert_eq!(
result.display_row_at(0),
Some(vec!["2".to_string(), "3".to_string()])
);
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn data_rows_identical_to_header_are_preserved() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let sql = "SELECT 1 AS id, 'Alice' AS name; \
SELECT 'id' AS id, 'name' AS name";
let result = adapter
.execute_adhoc(&dsn, sql, AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.columns, vec!["id", "name"]);
assert_eq!(
result.display_row_at(0),
Some(vec!["id".to_string(), "name".to_string()])
);
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn empty_leading_result_set_returns_last() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let sql = "SELECT 1 AS a WHERE false; SELECT 2 AS b";
let result = adapter
.execute_adhoc(&dsn, sql, AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.columns, vec!["b"]);
assert_eq!(result.display_row_at(0), Some(vec!["2".to_string()]));
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn empty_trailing_result_set_returns_zero_rows() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let sql = "SELECT 1 AS a; SELECT 2 AS b WHERE false";
let result = adapter
.execute_adhoc(&dsn, sql, AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.columns, vec!["b"]);
assert_eq!(result.data_row_count(), 0);
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn statements_share_one_session_and_transaction() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let sql = "CREATE TEMP TABLE boundary_probe(v int); \
INSERT INTO boundary_probe VALUES (7); \
SELECT v FROM boundary_probe";
let result = adapter
.execute_adhoc(&dsn, sql, AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.columns, vec!["v"]);
assert_eq!(result.display_row_at(0), Some(vec!["7".to_string()]));
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn write_only_statements_report_aggregate_tag() {
let adapter = PostgresAdapter::new();
let dsn = postgres_integration_dsn();
let sql = "CREATE TEMP TABLE tag_probe(v int); \
INSERT INTO tag_probe VALUES (1), (2)";
let result = adapter
.execute_adhoc(&dsn, sql, AccessMode::ReadWrite)
.await
.unwrap();
assert_eq!(result.data_row_count(), 0);
assert_eq!(
result.command_tag,
Some(CommandTag::Create("TABLE".to_string()))
);
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn error_in_later_statement_rolls_back_earlier_writes() {
with_postgres_test_db(|db| {
Box::pin(async move {
let failing = format!(
"CREATE TABLE \"{}\".boundary_rollback_probe(v int); SELECT no_such_column",
db.schema()
);
let result = db
.adapter()
.execute_adhoc(db.dsn(), &failing, AccessMode::ReadWrite)
.await;
if result.is_ok() {
return Err("expected mid-script error".to_string());
}
let check = db
.adapter()
.execute_adhoc(
db.dsn(),
&format!(
"SELECT to_regclass('{}.boundary_rollback_probe') IS NULL AS rolled_back",
db.schema()
),
AccessMode::ReadWrite,
)
.await
.map_err(|err| err.to_string())?;
if check.display_row_at(0) != Some(vec!["t".to_string()]) {
return Err(format!(
"expected rollback marker, got {:?}",
check.display_row_at(0)
));
}
Ok(())
})
})
.await;
}
}
mod error_paths {
use super::*;
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn bad_dsn_returns_connection_or_query_error() {
let adapter = PostgresAdapter::new();
let result = adapter.fetch_metadata(postgres_bad_dsn()).await;
assert!(result.is_err(), "Expected error for bad DSN");
}
#[tokio::test]
#[ignore = "requires PostgreSQL, tracked: #133"]
async fn timeout_with_pg_sleep_returns_timeout_error() {
let adapter = PostgresAdapter::with_timeout(1);
let dsn = postgres_integration_dsn();
let result = adapter
.execute_adhoc(&dsn, "SELECT pg_sleep(5)", AccessMode::ReadWrite)
.await;
assert!(
matches!(result, Err(DbOperationError::Timeout(_))),
"Expected Timeout error, got: {result:?}"
);
}
}