qraft-core 0.1.2

Core type system, query model, decoding, and SQL lowering primitives for qraft.
Documentation
use crate::{
    dialect::Dialect,
    expression::{Operator, PostfixOperator, UnaryOperator},
    instr::RpnInstr,
};

/// <https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html>
/// <https://www.postgresql.org/docs/18/sql-syntax-lexical.html#SQL-PRECEDENCE>
/// <https://dev.mysql.com/doc/refman/9.6/en/operator-precedence.html>
/// <https://sqlite.org/lang_expr.html#operators>
pub(crate) fn binding_power(op: &RpnInstr, dialect: Dialect) -> (u8, u8) {
    match dialect {
        Dialect::Postgres => match op {
            RpnInstr::Binary { op, .. } => match op {
                Operator::And => (3, 4),
                Operator::Or => (1, 2),
                Operator::Gt
                | Operator::Lt
                | Operator::Gte
                | Operator::Lte
                | Operator::Eq
                | Operator::Neq => (9, 10),
                Operator::Add | Operator::Sub => (13, 14),
                Operator::Mul | Operator::Div | Operator::Rem => (15, 16),
                Operator::Like { .. } => (11, 12),
                Operator::BitAnd => (13, 14),
                Operator::BitOr => (13, 14),
                Operator::Shl | Operator::Shr => (13, 14),
            },
            RpnInstr::Between { .. } => (11, 12),
            RpnInstr::Unary {
                op: UnaryOperator::Not,
                ..
            } => (6, 5),
            RpnInstr::Postfix {
                op: PostfixOperator::Null { .. } | PostfixOperator::False | PostfixOperator::True,
                ..
            } => (7, 8),
            _ => (0, 0),
        },
        Dialect::MariaDb => match op {
            RpnInstr::Unary {
                op: UnaryOperator::Not,
                ..
            } => (6, 5),
            RpnInstr::Binary { op, .. } => match op {
                Operator::And => (3, 4),
                Operator::Or => (1, 2),
                Operator::Gt
                | Operator::Lt
                | Operator::Gte
                | Operator::Lte
                | Operator::Eq
                | Operator::Neq => (9, 10),
                Operator::Add | Operator::Sub => (15, 16),
                Operator::Mul | Operator::Div | Operator::Rem => (16, 17),
                Operator::Like { .. } => (9, 10),
                Operator::BitAnd => (11, 12),
                Operator::BitOr => (11, 12),
                Operator::Shl | Operator::Shr => (13, 14),
            },
            RpnInstr::Between { .. } => (7, 8),
            RpnInstr::Postfix {
                op: PostfixOperator::Null { .. } | PostfixOperator::False | PostfixOperator::True,
                ..
            } => (9, 10),
            _ => (0, 0),
        },
        Dialect::Sqlite => match op {
            RpnInstr::Binary { op, .. } => match op {
                Operator::And => (3, 4),
                Operator::Or => (1, 2),
                Operator::Gt | Operator::Lt | Operator::Gte | Operator::Lte => (9, 10),
                Operator::Eq | Operator::Neq => (7, 8),
                Operator::Add | Operator::Sub => (13, 14),
                Operator::Mul | Operator::Div | Operator::Rem => (15, 16),
                Operator::Like { .. } => (7, 8),
                Operator::BitAnd => (11, 12),
                Operator::BitOr => (11, 12),
                Operator::Shl | Operator::Shr => (13, 14),
            },
            RpnInstr::Unary {
                op: UnaryOperator::Not,
                ..
            } => (6, 5),
            RpnInstr::Postfix {
                op: PostfixOperator::Null { .. } | PostfixOperator::False | PostfixOperator::True,
                ..
            } => (7, 8),
            RpnInstr::Between { .. } => (7, 8),
            _ => (0, 0),
        },
    }
}

#[cfg(test)]
mod tests {
    use super::binding_power;
    use crate::{
        dialect::Dialect,
        expression::{Operator, PostfixOperator, UnaryOperator},
        instr::RpnInstr,
    };

    #[test]
    fn binding_power_matches_expected_binary_operators_for_all_dialects() {
        let cases = [
            (
                Dialect::Postgres,
                RpnInstr::Binary {
                    op: Operator::Eq,
                    lhs: 1,
                    rhs: 1,
                },
                (9, 10),
            ),
            (
                Dialect::Postgres,
                RpnInstr::Binary {
                    op: Operator::Mul,
                    lhs: 1,
                    rhs: 1,
                },
                (15, 16),
            ),
            (
                Dialect::MariaDb,
                RpnInstr::Binary {
                    op: Operator::Like {
                        sensitive: false,
                        negated: false,
                    },
                    lhs: 1,
                    rhs: 1,
                },
                (9, 10),
            ),
            (
                Dialect::MariaDb,
                RpnInstr::Binary {
                    op: Operator::BitAnd,
                    lhs: 1,
                    rhs: 1,
                },
                (11, 12),
            ),
            (
                Dialect::Sqlite,
                RpnInstr::Binary {
                    op: Operator::Eq,
                    lhs: 1,
                    rhs: 1,
                },
                (7, 8),
            ),
            (
                Dialect::Sqlite,
                RpnInstr::Binary {
                    op: Operator::Shl,
                    lhs: 1,
                    rhs: 1,
                },
                (13, 14),
            ),
        ];

        for (dialect, instr, expected) in cases {
            assert_eq!(binding_power(&instr, dialect), expected);
        }
    }

    #[test]
    fn postgres_binding_power_matches_qrafting_shape() {
        assert_eq!(
            binding_power(
                &RpnInstr::Binary {
                    op: Operator::And,
                    lhs: 1,
                    rhs: 1,
                },
                Dialect::Postgres,
            ),
            (3, 4)
        );
        assert_eq!(
            binding_power(
                &RpnInstr::Binary {
                    op: Operator::Like {
                        sensitive: false,
                        negated: false,
                    },
                    lhs: 1,
                    rhs: 1,
                },
                Dialect::Postgres,
            ),
            (11, 12)
        );
        assert_eq!(
            binding_power(
                &RpnInstr::Unary {
                    op: UnaryOperator::Not,
                    rhs: 1,
                },
                Dialect::Postgres,
            ),
            (6, 5)
        );
        assert_eq!(
            binding_power(
                &RpnInstr::Postfix {
                    op: PostfixOperator::True,
                    lhs: 1,
                },
                Dialect::Postgres,
            ),
            (7, 8)
        );
    }

    #[test]
    fn binding_power_matches_unary_and_postfix_operators_for_all_dialects() {
        for (dialect, postfix_expected) in [
            (Dialect::Postgres, (7, 8)),
            (Dialect::MariaDb, (9, 10)),
            (Dialect::Sqlite, (7, 8)),
        ] {
            assert_eq!(
                binding_power(
                    &RpnInstr::Unary {
                        op: UnaryOperator::Not,
                        rhs: 1,
                    },
                    dialect,
                ),
                (6, 5)
            );
            assert_eq!(
                binding_power(
                    &RpnInstr::Between {
                        lhs: 1,
                        low: 1,
                        high: 1,
                        negated: false,
                    },
                    dialect,
                ),
                match dialect {
                    Dialect::Postgres => (11, 12),
                    Dialect::MariaDb | Dialect::Sqlite => (7, 8),
                }
            );
            assert_eq!(
                binding_power(
                    &RpnInstr::Postfix {
                        op: PostfixOperator::Null { negated: false },
                        lhs: 1,
                    },
                    dialect,
                ),
                postfix_expected
            );
        }
    }
}