faucet-common-redshift 1.2.3

Shared connection, credentials, and connection-pool types for the faucet-stream Amazon Redshift source and sink connectors
Documentation
use super::*;
use aws_sdk_redshiftdata::primitives::Blob;
use serde_json::json;

fn col(name: &str, ty: &str) -> DataColumn {
    DataColumn {
        name: name.into(),
        type_name: ty.into(),
        nullable: None,
    }
}

#[test]
fn placeholders_become_named_parameters_with_nulls_and_empties_inline() {
    let st = named_parameters(
        "SELECT * FROM t WHERE a = $1 AND b = CAST($2 AS TIMESTAMP) AND c = $3 AND d = $4 \
             AND e = $1 AND f = $10 AND g = '$2' AND \"$3\" = 1 -- $4\n AND h = $5 /* $5 */ AND x$1 = 2",
        &[
            json!(7),
            json!("2026-01-01"),
            json!(null),
            json!(""),
            json!({"k": [1]}),
        ],
    );
    assert_eq!(
        st.sql,
        "SELECT * FROM t WHERE a = :p1 AND b = CAST(:p2 AS TIMESTAMP) AND c = NULL AND d = '' \
             AND e = :p1 AND f = $10 AND g = '$2' AND \"$3\" = 1 -- $4\n AND h = :p5 /* $5 */ AND x$1 = 2"
    );
    assert_eq!(
        st.parameters,
        vec![
            ("p1".to_string(), "7".to_string()),
            ("p2".to_string(), "2026-01-01".to_string()),
            ("p5".to_string(), r#"{"k":[1]}"#.to_string()),
        ]
    );
    let st = named_parameters("SELECT 'it''s $1', $1, 'unterminated $1", &[json!(true)]);
    assert_eq!(st.sql, "SELECT 'it''s $1', :p1, 'unterminated $1");
    assert_eq!(st.parameters, vec![("p1".into(), "true".into())]);
    assert_eq!(named_parameters("SELECT $1", &[]).sql, "SELECT $1");
    assert_eq!(Statement::new("SELECT 1").parameters, vec![]);
}

#[test]
fn typed_fields_render_through_fidelity() {
    let cases: Vec<(DataColumn, Field, Value)> = vec![
        (col("n", "int8"), Field::IsNull(true), Value::Null),
        (
            col("i", "int8"),
            Field::LongValue(i64::MIN),
            json!(i64::MIN),
        ),
        (col("f", "float8"), Field::DoubleValue(0.1), json!(0.1)),
        (
            col("f4", "float4"),
            Field::DoubleValue(0.1f32 as f64),
            json!(0.1),
        ),
        (col("b", "bool"), Field::BooleanValue(true), json!(true)),
        (
            col("bin", "varbyte"),
            Field::BlobValue(Blob::new(vec![0, 1, 2, 253, 254, 255])),
            json!("AAEC/f7/"),
        ),
        (
            col("d", "numeric"),
            Field::StringValue("12345678901234567890.123456789".into()),
            json!("12345678901234567890.123456789"),
        ),
        (
            col("s", "numeric"),
            Field::StringValue("1.50".into()),
            json!("1.50"),
        ),
        (
            col("big", "int8"),
            Field::StringValue("123".into()),
            json!(123),
        ),
        (
            col("ft", "float8"),
            Field::StringValue("NaN".into()),
            Value::Null,
        ),
        (
            col("ft", "float8"),
            Field::StringValue("1.5".into()),
            json!(1.5),
        ),
        (
            col("bt", "bool"),
            Field::StringValue("f".into()),
            json!(false),
        ),
        (
            col("tz", "timestamptz"),
            Field::StringValue("2026-09-18 12:34:56.123456+05:30".into()),
            json!("2026-09-18T12:34:56.123456+05:30"),
        ),
        (
            col("tz", "timestamptz"),
            Field::StringValue("2026-09-18 12:34:56+00".into()),
            json!("2026-09-18T12:34:56.000000Z"),
        ),
        (
            col("ts", "timestamp"),
            Field::StringValue("1969-07-20 20:17:40".into()),
            json!("1969-07-20T20:17:40.000000"),
        ),
        (
            col("ts", "timestamp"),
            Field::StringValue("infinity".into()),
            json!("infinity"),
        ),
        (
            col("tm", "time"),
            Field::StringValue("01:02:03.5".into()),
            json!("01:02:03.500000"),
        ),
        (
            col("bx", "bytea"),
            Field::StringValue("\\x00ff".into()),
            json!("AP8="),
        ),
        (
            col("js", "json"),
            Field::StringValue(r#"{"a":123456789012345678901234567890}"#.into()),
            json!({"a": "123456789012345678901234567890"}),
        ),
        (
            col("v", "varchar"),
            Field::StringValue("it's".into()),
            json!("it's"),
        ),
        (
            col("dt", "date"),
            Field::StringValue("2026-02-28".into()),
            json!("2026-02-28"),
        ),
    ];
    for (c, f, want) in cases {
        assert_eq!(field_to_json(&c, &f).unwrap(), want, "{c:?} {f:?}");
    }
    for (c, f, needle) in [
        (col("d", "numeric"), Field::StringValue("abc".into()), "`d`"),
        (
            col("b", "bool"),
            Field::StringValue("yes".into()),
            "boolean",
        ),
        (
            col("f", "float8"),
            Field::StringValue("x".into()),
            "not a number",
        ),
        (col("v", "varbyte"), Field::StringValue("abc".into()), "hex"),
        (col("n", "int4"), Field::IsNull(false), "isNull"),
    ] {
        let e = field_to_json(&c, &f).unwrap_err().to_string();
        assert!(e.contains(needle), "{e}");
    }
}

#[test]
fn rows_map_by_column_and_refuse_a_width_mismatch() {
    let cols = [col("id", "int4"), col("name", "varchar")];
    let row = row_to_json(
        &cols,
        &[Field::LongValue(1), Field::StringValue("a".into())],
    )
    .unwrap();
    assert_eq!(Value::Object(row), json!({"id": 1, "name": "a"}));
    assert!(row_to_json(&cols, &[Field::LongValue(1)]).is_err());
}

#[test]
fn data_api_targets_are_validated() {
    let conn = |cfg: Value| -> RedshiftConnection {
        serde_json::from_value(json!({
            "database": "dev",
            "credentials": {"type": "redshift_data_api", "config": cfg}
        }))
        .unwrap()
    };
    let ok = DataApiClient::from_connection(
        &conn(json!({"cluster_identifier": "c", "db_user": "u"})),
        FaucetError::Source,
    )
    .unwrap()
    .unwrap();
    assert!(format!("{ok:?}").contains("Cluster"));
    assert!(
        DataApiClient::from_connection(&conn(json!({"workgroup_name": "w"})), FaucetError::Source)
            .unwrap()
            .is_some()
    );
    assert!(
        DataApiClient::from_connection(
            &RedshiftConnection::new("h", "d", "u", "p"),
            FaucetError::Source
        )
        .unwrap()
        .is_none()
    );
    let mut no_db = conn(json!({"workgroup_name": "w"}));
    no_db.database = String::new();
    for (c, needle) in [
        (no_db, "database"),
        (
            conn(json!({"cluster_identifier": "c"})),
            "exactly one of `db_user`",
        ),
        (
            conn(json!({"cluster_identifier": "c", "db_user": "u", "secret_arn": "a"})),
            "exactly one of `db_user`",
        ),
        (
            conn(json!({"workgroup_name": "w", "db_user": "u"})),
            "provisioned",
        ),
        (conn(json!({})), "exactly one of `cluster_identifier`"),
    ] {
        let e = DataApiClient::from_connection(&c, FaucetError::Source)
            .unwrap_err()
            .to_string();
        assert!(e.contains(needle), "{e}");
    }
}

#[test]
fn hex_decoding() {
    assert_eq!(hex_bytes("0aff"), Some(vec![10, 255]));
    assert_eq!(hex_bytes("\\x"), Some(vec![]));
    assert_eq!(hex_bytes("0"), None);
    assert_eq!(hex_bytes("zz"), None);
}