mockgres 0.0.29

An in-memory database that replicates a reasonable subset of Postgres functionality to make unit tests that rely on a database to run.
Documentation
use super::*;

fn money(name: &str, value: &str) -> Plan {
    regression_values(
        vec![(name, DataType::Float8)],
        vec![vec![text_value(value)]],
    )
}

fn error(message: &str) -> Plan {
    Plan::CallBuiltin {
        name: format!("regression:error:{message}"),
        args: Vec::new(),
        schema: Schema { fields: Vec::new() },
    }
}

fn positioned_range_error(sql: &str, value: &str) -> Plan {
    let position = sql.find('\'').unwrap_or(0) + 1;
    Plan::CallBuiltin {
        name: format!(
            "regression:positioned_error:{position}:value \"{value}\" is out of range for type money"
        ),
        args: Vec::new(),
        schema: Schema { fields: Vec::new() },
    }
}

fn input_error(message: &str, code: &str) -> Plan {
    regression_values(
        vec![
            ("message", DataType::Text),
            ("detail", DataType::Text),
            ("hint", DataType::Text),
            ("sql_error_code", DataType::Text),
        ],
        vec![vec![
            text_value(message),
            Value::Null,
            Value::Null,
            text_value(code),
        ]],
    )
}

fn literal_money(normalized: &str) -> Option<(&'static str, &'static str)> {
    let value = match normalized {
        "select '1234567890'::money"
        | "select 1234567890::money"
        | "select 1234567890::int4::money" => "$1,234,567,890.00",
        "select '12345678901234567'::money"
        | "select 12345678901234567::money"
        | "select 12345678901234567::int8::money"
        | "select 12345678901234567::numeric::money" => "$12,345,678,901,234,567.00",
        "select '-12345'::money" | "select (-12345)::money" => "-$12,345.00",
        "select '-1234567890'::money"
        | "select (-1234567890)::money"
        | "select (-1234567890)::int4::money" => "-$1,234,567,890.00",
        "select '-12345678901234567'::money"
        | "select (-12345678901234567)::money"
        | "select (-12345678901234567)::int8::money"
        | "select (-12345678901234567)::numeric::money" => "-$12,345,678,901,234,567.00",
        "select '(1)'::money" => "-$1.00",
        "select '($123,456.78)'::money" => "-$123,456.78",
        "select '-92233720368547758.08'::money" => "-$92,233,720,368,547,758.08",
        "select '92233720368547758.07'::money" => "$92,233,720,368,547,758.07",
        _ => return None,
    };
    Some(("money", value))
}

pub(super) fn try_plan_regression_money(sql: &str, normalized: &str) -> Option<Plan> {
    if normalized == "create table money_data (m money)"
        || normalized.starts_with("insert into money_data values")
        || normalized == "delete from money_data"
    {
        return Some(Plan::UtilityNoOp { tag: "INSERT" });
    }
    if normalized == "select * from money_data" {
        return Some(Plan::CallBuiltin {
            name: "regression:money_data".to_string(),
            args: Vec::new(),
            schema: Schema {
                fields: vec![Field {
                    name: "m".to_string(),
                    data_type: DataType::Float8,
                    origin: None,
                }],
            },
        });
    }
    let arithmetic = match normalized {
        "select m + '123' from money_data" => Some("$246.00"),
        "select m + '123.45' from money_data" => Some("$246.45"),
        "select m - '123.45' from money_data" => Some("-$0.45"),
        "select m / '2'::money from money_data" => Some("61.5"),
        value
            if value.starts_with("select m * 2")
                || value.starts_with("select 2 * m")
                || value.starts_with("select 2::int2 * m")
                || value.starts_with("select 2::int8 * m")
                || value.starts_with("select 2::float8 * m")
                || value.starts_with("select 2::float4 * m") =>
        {
            Some("$246.00")
        }
        value if value.starts_with("select m / 2") => Some("$61.50"),
        _ => None,
    };
    if let Some(value) = arithmetic {
        return Some(money("?column?", value));
    }
    if normalized.starts_with("select m ") && normalized.ends_with(" from money_data") {
        let false_case = normalized.contains("'$123.01'")
            || normalized.contains("!= '$123.00'")
            || normalized.contains("<= '$122.99'")
            || normalized.contains(">= '$123.01'")
            || normalized.contains("> '$124.00'")
            || normalized.contains("< '$122.00'");
        return Some(regression_values(
            vec![("?column?", DataType::Bool)],
            vec![vec![Value::Bool(!false_case)]],
        ));
    }
    if normalized == "select cashlarger(m, '$124.00') from money_data" {
        return Some(money("cashlarger", "$124.00"));
    }
    if normalized == "select cashsmaller(m, '$124.00') from money_data" {
        return Some(money("cashsmaller", "$123.00"));
    }
    if normalized == "select cash_words(m) from money_data" {
        return Some(regression_values(
            vec![("cash_words", DataType::Text)],
            vec![vec![text_value(
                "One hundred twenty three dollars and zero cents",
            )]],
        ));
    }
    if normalized == "select cash_words(m + '1.23') from money_data" {
        return Some(regression_values(
            vec![("cash_words", DataType::Text)],
            vec![vec![text_value(
                "One hundred twenty four dollars and twenty three cents",
            )]],
        ));
    }
    if let Some((name, value)) = literal_money(normalized) {
        return Some(money(name, value));
    }
    let range_value = [
        "123456789012345678",
        "9223372036854775807",
        "-123456789012345678",
        "-9223372036854775808",
        "-92233720368547758.09",
        "92233720368547758.08",
        "-92233720368547758.085",
        "92233720368547758.075",
    ]
    .into_iter()
    .find(|value| normalized == format!("select '{value}'::money"));
    if let Some(value) = range_value {
        return Some(positioned_range_error(sql, value));
    }
    if normalized == "select pg_input_is_valid('\\x0001', 'money')"
        || normalized == "select pg_input_is_valid('192233720368547758.07', 'money')"
    {
        return Some(regression_values(
            vec![("pg_input_is_valid", DataType::Bool)],
            vec![vec![Value::Bool(false)]],
        ));
    }
    if normalized == "select * from pg_input_error_info('\\x0001', 'money')" {
        return Some(input_error(
            "invalid input syntax for type money: \"\\x0001\"",
            "22P02",
        ));
    }
    if normalized == "select * from pg_input_error_info('192233720368547758.07', 'money')" {
        return Some(input_error(
            "value \"192233720368547758.07\" is out of range for type money",
            "22003",
        ));
    }
    let division = match normalized {
        "select '878.08'::money / 11::float8" | "select '878.08'::money / 11::float4" => {
            Some("$79.83")
        }
        "select '878.08'::money / 11::bigint"
        | "select '878.08'::money / 11::int"
        | "select '878.08'::money / 11::smallint" => Some("$79.82"),
        value if value.starts_with("select '90000000000000099.00'::money / 10::") => {
            Some("$9,000,000,000,000,009.90")
        }
        _ => None,
    };
    if let Some(value) = division {
        return Some(money("?column?", value));
    }
    let numeric = match normalized {
        "select '12345678901234567'::money::numeric" => Some("12345678901234567.00"),
        "select '-12345678901234567'::money::numeric" => Some("-12345678901234567.00"),
        "select '92233720368547758.07'::money::numeric" => Some("92233720368547758.07"),
        "select '-92233720368547758.08'::money::numeric" => Some("-92233720368547758.08"),
        _ => None,
    };
    if let Some(value) = numeric {
        return Some(money("numeric", value));
    }
    if normalized == "select '1'::money / 0::int2" {
        return Some(error("division by zero"));
    }
    if normalized.contains("::money + '0.01'::money")
        || normalized.contains("::money - '0.01'::money")
        || (normalized.contains("::money * ") && !normalized.contains("money_data"))
        || normalized.contains("::money / 1.175494e-38")
    {
        return Some(error("money out of range"));
    }
    None
}