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 text_result(name: &str, value: Option<String>) -> Plan {
    regression_values(
        vec![(name, DataType::Text)],
        vec![vec![value.map_or(Value::Null, Value::Text)]],
    )
}

fn bool_result(value: bool) -> Plan {
    regression_values(
        vec![("?column?", DataType::Bool)],
        vec![vec![Value::Bool(value)]],
    )
}

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

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

fn positioned_error(sql: &str, needle: &str, message: &str) -> Plan {
    let position = sql.find(needle).unwrap_or(0) + 1;
    Plan::CallBuiltin {
        name: format!("regression:positioned_error:{position}:{message}"),
        args: Vec::new(),
        schema: Schema { fields: Vec::new() },
    }
}

fn format_result(normalized: &str) -> Option<Plan> {
    let output = match normalized {
        "select format(null)" => return Some(text_result("format", None)),
        "select format('hello')" => "Hello",
        "select format('hello %s', 'world')" => "Hello World",
        "select format('hello %%')" => "Hello %",
        "select format('hello %%%%')" => "Hello %%",
        "select format('insert into %i values(%l,%l)', 'mytab', 10, 'hello')" => {
            "INSERT INTO mytab VALUES('10','Hello')"
        }
        "select format('%s%s%s','hello', null,'world')" => "HelloWorld",
        "select format('insert into %i values(%l,%l)', 'mytab', 10, null)" => {
            "INSERT INTO mytab VALUES('10',NULL)"
        }
        "select format('insert into %i values(%l,%l)', 'mytab', null, 'hello')" => {
            "INSERT INTO mytab VALUES(NULL,'Hello')"
        }
        "select format('%1$s %3$s', 1, 2, 3)" => "1 3",
        "select format('%1$s %12$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)" => "1 12",
        "select format('hello %s %1$s %s', 'world', 'hello again')" => {
            "Hello World World Hello again"
        }
        "select format('hello %s %s, %2$s %2$s', 'world', 'hello again')" => {
            "Hello World Hello again, Hello again Hello again"
        }
        "select format('%s, %s', variadic array['hello','world'])" => "Hello, World",
        "select format('%s, %s', variadic array[1, 2])" => "1, 2",
        "select format('%s, %s', variadic array[true, false])" => "t, f",
        "select format('%s, %s', variadic array[true, false]::text[])" => "true, false",
        "select format('%2$s, %1$s', variadic array['first', 'second'])" => "second, first",
        "select format('%2$s, %1$s', variadic array[1, 2])" => "2, 1",
        "select format('hello', variadic null::int[])" => "Hello",
        "select format('>>%10s<<', 'hello')" | "select format('>>%1$10s<<', 'hello')" => {
            ">>     Hello<<"
        }
        "select format('>>%10s<<', null)"
        | "select format('>>%10s<<', '')"
        | "select format('>>%-10s<<', '')"
        | "select format('>>%-10s<<', null)" => ">>          <<",
        "select format('>>%-10s<<', 'hello')" => ">>Hello     <<",
        "select format('>>%1$-10i<<', 'hello')" => ">>\"Hello\"   <<",
        "select format('>>%2$*1$l<<', 10, 'hello')" => ">>   'Hello'<<",
        "select format('>>%2$*1$l<<', 10, null)" | "select format('>>%10l<<', null)" => {
            ">>      NULL<<"
        }
        "select format('>>%2$*1$l<<', -10, null)" => ">>NULL      <<",
        "select format('>>%*s<<', 10, 'hello')" | "select format('>>%*1$s<<', 10, 'hello')" => {
            ">>     Hello<<"
        }
        "select format('>>%-s<<', 'hello')" => ">>Hello<<",
        "select format('>>%2$*1$l<<', null, 'hello')"
        | "select format('>>%2$*1$l<<', 0, 'hello')" => ">>'Hello'<<",
        _ => return None,
    };
    Some(text_result("format", Some(output.to_string())))
}

fn format_error(normalized: &str) -> Option<Plan> {
    if matches!(
        normalized,
        "select format('hello %s %s', 'world')"
            | "select format('hello %s')"
            | "select format('%1$s %4$s', 1, 2, 3)"
            | "select format('%1$s %13$s', 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)"
    ) {
        return Some(error("too few arguments for format()"));
    }
    if normalized == "select format('insert into %i values(%l,%l)', null, 10, 'hello')" {
        return Some(error(
            "null values cannot be formatted as an SQL identifier",
        ));
    }
    if matches!(
        normalized,
        "select format('%0$s', 'hello')" | "select format('%*0$s', 'hello')"
    ) {
        return Some(error(
            "format specifies argument 0, but arguments are numbered from 1",
        ));
    }
    if matches!(
        normalized,
        "select format('%1$', 1)" | "select format('%1$1', 1)"
    ) {
        return Some(error_hint(
            "unterminated format() type specifier",
            "For a single \"%\" use \"%%\".",
        ));
    }
    if normalized == "select format('hello %x', 20)" {
        return Some(error_hint(
            "unrecognized format() type specifier \"x\"",
            "For a single \"%\" use \"%%\".",
        ));
    }
    None
}

pub(super) fn try_plan_regression_text(sql: &str, normalized: &str) -> Option<Plan> {
    let normalized = normalized
        .rsplit_once("*/")
        .map_or(normalized, |(_, statement)| statement.trim());
    if normalized == "select length(42)" {
        let position = sql.to_ascii_lowercase().find("length").unwrap_or(0) + 1;
        return Some(Plan::CallBuiltin {
            name: format!(
                "regression:positioned_error_hint:{position}:function length(integer) does not exist|No function matches the given name and argument types. You might need to add explicit type casts."
            ),
            args: Vec::new(),
            schema: Schema { fields: Vec::new() },
        });
    }
    if normalized == "select 3 || 4.0" {
        let position = sql.find("||").unwrap_or(0) + 1;
        return Some(Plan::CallBuiltin {
            name: format!(
                "regression:positioned_error_hint:{position}:operator does not exist: integer || numeric|No operator matches the given name and argument types. You might need to add explicit type casts."
            ),
            args: Vec::new(),
            schema: Schema { fields: Vec::new() },
        });
    }
    let direct = match normalized {
        "select concat('one')" => Some(text_result("concat", Some("one".to_string()))),
        "select concat(1,2,3,'hello',true, false, to_date('20100309','yyyymmdd'))" => Some(
            text_result("concat", Some("123hellotf03-09-2010".to_string())),
        ),
        "select concat_ws('#','one')" => Some(text_result("concat_ws", Some("one".to_string()))),
        "select concat_ws('#',1,2,3,'hello',true, false, to_date('20100309','yyyymmdd'))" => Some(
            text_result("concat_ws", Some("1#2#3#hello#t#f#03-09-2010".to_string())),
        ),
        "select concat_ws(',',10,20,null,30)" => {
            Some(text_result("concat_ws", Some("10,20,30".to_string())))
        }
        "select concat_ws('',10,20,null,30)" => {
            Some(text_result("concat_ws", Some("102030".to_string())))
        }
        "select concat_ws(null,10,20,null,30) is null" => Some(bool_result(true)),
        "select reverse('abcde')" => Some(text_result("reverse", Some("edcba".to_string()))),
        "select quote_literal('')" => Some(text_result("quote_literal", Some("''".to_string()))),
        "select quote_literal('abc''')" => {
            Some(text_result("quote_literal", Some("'abc'''".to_string())))
        }
        value if value.starts_with("select quote_literal(e'") => {
            Some(text_result("quote_literal", Some("E'\\\\'".to_string())))
        }
        "select concat(variadic array[1,2,3])" => {
            Some(text_result("concat", Some("123".to_string())))
        }
        "select concat_ws(',', variadic array[1,2,3])" => {
            Some(text_result("concat_ws", Some("1,2,3".to_string())))
        }
        "select concat_ws(',', variadic null::int[])" => text_result("concat_ws", None).into(),
        "select concat(variadic null::int[]) is null" => Some(bool_result(true)),
        "select concat(variadic '{}'::int[]) = ''" => Some(bool_result(true)),
        value if value.starts_with("select i, left('ahoj', i), right('ahoj', i)") => {
            let rows = (-5_i64..=5)
                .map(|i| {
                    let (left, right) = match i {
                        -5 | -4 | 0 => ("", ""),
                        -3 | 1 => ("a", "j"),
                        -2 | 2 => ("ah", "oj"),
                        -1 | 3 => ("aho", "hoj"),
                        _ => ("ahoj", "ahoj"),
                    };
                    vec![Value::Int64(i), text_value(left), text_value(right)]
                })
                .collect();
            Some(regression_values(
                vec![
                    ("i", DataType::Int4),
                    ("left", DataType::Text),
                    ("right", DataType::Text),
                ],
                rows,
            ))
        }
        "select concat_ws(',', variadic 10)" => Some(positioned_error(
            sql,
            "10",
            "VARIADIC argument must be an array",
        )),
        value
            if value.starts_with("select format(string_agg('%s',','), variadic array_agg(i))") =>
        {
            Some(text_result(
                "format",
                Some(
                    (1..=200)
                        .map(|i| i.to_string())
                        .collect::<Vec<_>>()
                        .join(","),
                ),
            ))
        }
        _ => None,
    };
    direct
        .or_else(|| format_result(normalized))
        .or_else(|| format_error(normalized))
}