gluesql-core 0.20.0

GlueSQL - Open source SQL database engine fully written in Rust with pure functional execution layer, easily swappable storage and web assembly support!
Documentation
use crate::{
    data::Value,
    plan::{ExprPlan, FunctionExprPlan},
};

pub fn may_return_null(expr: &ExprPlan) -> bool {
    match expr {
        ExprPlan::Value(Value::Null)
        | ExprPlan::Identifier(_)
        | ExprPlan::CompoundIdentifier { .. }
        | ExprPlan::ArrayIndex { .. }
        | ExprPlan::Subquery(_)
        | ExprPlan::Exists { .. }
        | ExprPlan::InSubquery { .. }
        | ExprPlan::Aggregate(_) => true,
        ExprPlan::Literal(_)
        | ExprPlan::Value(_)
        | ExprPlan::TypedString { .. }
        | ExprPlan::IsNull(_)
        | ExprPlan::IsNotNull(_) => false,
        ExprPlan::UnaryOp { expr: inner, .. }
        | ExprPlan::Nested(inner)
        | ExprPlan::Interval { expr: inner, .. } => may_return_null(inner),
        ExprPlan::BinaryOp { left, right, .. }
        | ExprPlan::Like {
            expr: left,
            pattern: right,
            ..
        }
        | ExprPlan::ILike {
            expr: left,
            pattern: right,
            ..
        }
        | ExprPlan::Regex {
            expr: left,
            pattern: right,
            ..
        } => may_return_null(left) || may_return_null(right),
        ExprPlan::Between {
            expr, low, high, ..
        } => may_return_null(expr) || may_return_null(low) || may_return_null(high),
        ExprPlan::InList { expr, list, .. } => {
            may_return_null(expr) || list.iter().any(may_return_null)
        }
        ExprPlan::Function(function) => function_may_return_null(function),
        ExprPlan::Case {
            operand,
            when_then,
            else_result,
        } => {
            operand.as_deref().is_some_and(may_return_null)
                || when_then
                    .iter()
                    .any(|(when, then)| may_return_null(when) || may_return_null(then))
                || else_result.as_deref().is_none_or(may_return_null)
        }
        ExprPlan::Array { elem } => elem.iter().any(may_return_null),
    }
}

fn function_may_return_null(function: &FunctionExprPlan) -> bool {
    use FunctionExprPlan::*;

    match function {
        Coalesce(exprs) => exprs.iter().all(may_return_null),
        IfNull { expr, then } => may_return_null(expr) && may_return_null(then),
        NullIf { .. } | Custom { .. } => true,
        Now() | CurrentDate() | CurrentTime() | CurrentTimestamp() | Pi() | GenerateUuid()
        | Rand(_) => false,
        Cast { expr, .. }
        | Abs(expr)
        | Initcap(expr)
        | Lower(expr)
        | Upper(expr)
        | Asin(expr)
        | Acos(expr)
        | Atan(expr)
        | Ceil(expr)
        | Round(expr)
        | Trunc(expr)
        | Floor(expr)
        | Exp(expr)
        | Ln(expr)
        | Log2(expr)
        | Log10(expr)
        | Sin(expr)
        | Cos(expr)
        | Tan(expr)
        | Sqrt(expr)
        | Radians(expr)
        | Degrees(expr)
        | LastDay(expr)
        | Reverse(expr)
        | Sign(expr)
        | IsEmpty(expr)
        | Length(expr)
        | Entries(expr)
        | Keys(expr)
        | Values(expr)
        | Ascii(expr)
        | Chr(expr)
        | Md5(expr)
        | Hex(expr)
        | GetX(expr)
        | GetY(expr)
        | Dedup(expr)
        | Extract { expr, .. }
        | Sort { expr, order: None } => may_return_null(expr),
        AddMonth { expr, size }
        | Left { expr, size }
        | Right { expr, size }
        | Repeat { expr, num: size }
        | Skip { expr, size }
        | Take { expr, size }
        | Append { expr, value: size }
        | Prepend { expr, value: size }
        | Div {
            dividend: expr,
            divisor: size,
        }
        | Mod {
            dividend: expr,
            divisor: size,
        }
        | Gcd {
            left: expr,
            right: size,
        }
        | Lcm {
            left: expr,
            right: size,
        }
        | Power { expr, power: size }
        | Format { expr, format: size }
        | ToDate { expr, format: size }
        | ToTimestamp { expr, format: size }
        | ToTime { expr, format: size }
        | Position {
            from_expr: size,
            sub_expr: expr,
        }
        | Point { x: expr, y: size }
        | CalcDistance {
            geometry1: expr,
            geometry2: size,
        }
        | Sort {
            expr,
            order: Some(size),
        } => may_return_null(expr) || may_return_null(size),
        Log { antilog, base } => may_return_null(antilog) || may_return_null(base),
        Concat(exprs) | Greatest(exprs) => exprs.iter().any(may_return_null),
        ConcatWs { separator, exprs } => {
            may_return_null(separator) || exprs.iter().any(may_return_null)
        }
        Replace { expr, old, new } => {
            may_return_null(expr) || may_return_null(old) || may_return_null(new)
        }
        Lpad { expr, size, fill } | Rpad { expr, size, fill } => {
            may_return_null(expr)
                || may_return_null(size)
                || fill.as_ref().is_some_and(may_return_null)
        }
        Trim {
            expr, filter_chars, ..
        } => may_return_null(expr) || filter_chars.as_ref().is_some_and(may_return_null),
        Ltrim { expr, chars } | Rtrim { expr, chars } => {
            may_return_null(expr) || chars.as_ref().is_some_and(may_return_null)
        }
        Slice {
            expr,
            start,
            length,
        } => may_return_null(expr) || may_return_null(start) || may_return_null(length),
        Substr { expr, start, count } => {
            may_return_null(expr)
                || may_return_null(start)
                || count.as_ref().is_some_and(may_return_null)
        }
        Unwrap { expr, selector } => may_return_null(expr) || may_return_null(selector),
        FindIdx {
            from_expr,
            sub_expr,
            start,
        } => {
            may_return_null(from_expr)
                || may_return_null(sub_expr)
                || start.as_ref().is_some_and(may_return_null)
        }
        Splice {
            list_data,
            begin_index,
            end_index,
            values,
        } => {
            may_return_null(list_data)
                || may_return_null(begin_index)
                || may_return_null(end_index)
                || values.as_ref().is_some_and(may_return_null)
        }
    }
}

#[cfg(test)]
mod tests {
    use {
        super::may_return_null,
        crate::{
            parse_sql::parse_expr,
            translate::{NO_PARAMS, translate_expr},
        },
    };

    fn test(sql: &str, expected: bool) {
        let expr = parse_expr(sql).and_then(|parsed| translate_expr(&parsed, NO_PARAMS));
        let actual = expr.map(|expr| may_return_null(&expr.into()));

        assert_eq!(actual, Ok(expected), "{sql} nullability mismatch");
    }

    #[test]
    fn expression_cases() {
        test("NULL", true);
        test("id", true);
        test("Foo.id", true);
        test("COALESCE(NULL, id)", true);
        test("IFNULL(id, NULL)", true);
        test("CASE 1 WHEN 1 THEN id ELSE 0 END", true);
        test("1 IN (SELECT 1)", true);
        test("id IN (1, 2, 3)", true);
        test("ARRAY[1, id]", true);
        test("POSITION('a' IN id)", true);

        test("1", false);
        test("'A'", false);
        test("INTERVAL 1 DAY", false);
        test("NOT TRUE", false);
        test("1 BETWEEN 0 AND 2", false);
        test("'A' LIKE 'A%'", false);
        test("'A' ILIKE 'A%'", false);
        test("'A' ~ 'A'", false);
        test("NULL ~ 'A'", true);
        test("1 IN (1, 2, 3)", false);
        test("('A' IS NULL)", false);
        test("CASE 1 WHEN 1 THEN 2 ELSE 3 END", false);
    }

    #[test]
    fn function_branch_cases() {
        test("ABS(1)", false);
        test("ADD_MONTH(DATE '2020-01-01', 1)", false);
        test("LOWER('ABC')", false);
        test("INITCAP('abc')", false);
        test("UPPER('abc')", false);
        test("LEFT('abc', 1)", false);
        test("RIGHT('abc', 1)", false);
        test("ASIN(0)", false);
        test("ACOS(0)", false);
        test("ATAN(0)", false);
        test("LPAD('abc', 5, 'x')", false);
        test("RPAD('abc', 5, 'x')", false);
        test("REPLACE('abc', 'b', 'c')", false);
        test("CAST(1 AS INT)", false);
        test("CEIL(1.1)", false);
        test("COALESCE(1, 2)", false);
        test("CONCAT('a', 'b')", false);
        test("CONCAT_WS('-', 'a', 'b')", false);
        test("CUSTOM_FUNC()", true);
        test("IFNULL(1, 2)", false);
        test("NULLIF(1, 2)", true);
        test("RAND()", false);
        test("ROUND(1.2)", false);
        test("TRUNC(1.2)", false);
        test("FLOOR(1.2)", false);
        test("TRIM('  value  ')", false);
        test("EXP(1)", false);
        test("EXTRACT(YEAR FROM DATE '2020-01-01')", false);
        test("LN(1)", false);
        test("LOG(2, 10)", false);
        test("LOG2(2)", false);
        test("LOG10(10)", false);
        test("DIV(4, 2)", false);
        test("MOD(4, 2)", false);
        test("GCD(4, 2)", false);
        test("LCM(4, 2)", false);
        test("SIN(1)", false);
        test("COS(1)", false);
        test("TAN(1)", false);
        test("SQRT(4)", false);
        test("POWER(2, 3)", false);
        test("RADIANS(180)", false);
        test("DEGREES(3.14)", false);
        test("NOW()", false);
        test("CURRENT_DATE()", false);
        test("CURRENT_TIME()", false);
        test("CURRENT_TIMESTAMP()", false);
        test("PI()", false);
        test("LAST_DAY(DATE '2020-01-01')", false);
        test("LTRIM('  abc')", false);
        test("RTRIM('abc  ')", false);
        test("REVERSE('abc')", false);
        test("REPEAT('a', 2)", false);
        test("SIGN(1)", false);
        test("SUBSTR('abc', 1, 1)", false);
        test(r#"UNWRAP('{"a":1}', 'a')"#, false);
        test("GENERATE_UUID()", false);
        test("GREATEST(1, 2)", false);
        test("FORMAT('value', '%s')", false);
        test("TO_DATE('2020-01-01', '%Y-%m-%d')", false);
        test(
            "TO_TIMESTAMP('2020-01-01T00:00:00', '%Y-%m-%dT%H:%M:%S')",
            false,
        );
        test("TO_TIME('12:34:56', '%H:%M:%S')", false);
        test("POSITION('a' IN 'abc')", false);
        test("FIND_IDX('abc', 'a', 1)", false);
        test("ASCII('A')", false);
        test("CHR(65)", false);
        test("MD5('abc')", false);
        test("HEX('abc')", false);
        test("APPEND('[1,2]', '3')", false);
        test("SORT('[1,2]')", false);
        test("SORT('[1,2]', 'ASC')", false);
        test("SLICE('[1,2,3]', 1, 1)", false);
        test("PREPEND('[2,3]', '1')", false);
        test("SKIP('[1,2,3]', 1)", false);
        test("TAKE('[1,2,3]', 2)", false);
        test("GET_X('POINT(1 2)')", false);
        test("GET_Y('POINT(1 2)')", false);
        test("POINT(1, 2)", false);
        test("CALC_DISTANCE(POINT(0, 0), POINT(1, 1))", false);
        test("IS_EMPTY('[]')", false);
        test("LENGTH('[1,2]')", false);
        test(r#"ENTRIES('{"a":1}')"#, false);
        test(r#"KEYS('{"a":1}')"#, false);
        test(r#"VALUES('{"a":1}')"#, false);
        test("COALESCE(NULL)", true);
        test("IFNULL(NULL, NULL)", true);
        test("NULLIF(1, 1)", true);
        test("RAND(1)", false);
        test("COALESCE(NULL, NULL)", true);
        test("COALESCE()", true);
        test("SPLICE('[1,2,3]', 1, 2, '[9]')", false);
        test("DEDUP('[1,1,2]')", false);
    }

    #[test]
    #[should_panic(expected = "nullability mismatch")]
    fn invalid_expression_panics() {
        test("INVALID SQL", false);
    }
}