icydb-core 0.179.5

IcyDB — A schema-first typed query engine and persistence runtime for Internet Computer canisters
Documentation
use crate::{
    db::sql::parser::{
        SqlAggregateCall, SqlExpr, SqlExprBinaryOp, SqlOrderDirection, SqlOrderTerm, SqlProjection,
        SqlSelectItem, SqlSelectStatement, SqlStatement, parse_sql,
    },
    value::Value,
};

fn sql_order_expr(term: &str) -> SqlExpr {
    let sql = format!("SELECT id FROM NormalizeOrderEntity ORDER BY {term}");
    let SqlStatement::Select(statement) =
        parse_sql(&sql).expect("normalize ORDER BY term helper SQL should parse")
    else {
        unreachable!("normalize ORDER BY term helper should always produce one SELECT");
    };

    statement
        .order_by
        .into_iter()
        .next()
        .expect("normalize ORDER BY term helper SQL should carry one ORDER BY term")
        .field
}

#[test]
fn local_scalar_select_is_already_local_canonical() {
    let statement = SqlSelectStatement {
        entity: "PerfAuditUser".to_string(),
        table_alias: None,
        projection: SqlProjection::Items(vec![
            SqlSelectItem::Field("id".to_string()),
            SqlSelectItem::Field("age".to_string()),
        ]),
        projection_aliases: vec![None, None],
        predicate: Some(SqlExpr::Binary {
            op: SqlExprBinaryOp::And,
            left: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Ne,
                left: Box::new(SqlExpr::Field("age".to_string())),
                right: Box::new(SqlExpr::Literal(Value::Int64(24))),
            }),
            right: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Ne,
                left: Box::new(SqlExpr::Field("age".to_string())),
                right: Box::new(SqlExpr::Literal(Value::Int64(31))),
            }),
        }),
        distinct: false,
        group_by: vec![],
        having: vec![],
        order_by: vec![SqlOrderTerm {
            field: sql_order_expr("id"),
            direction: SqlOrderDirection::Asc,
        }],
        limit: Some(3),
        offset: None,
    };

    assert!(statement.is_already_local_canonical());
}

#[test]
fn local_scalar_select_with_supported_order_expr_is_already_local_canonical() {
    let statement = SqlSelectStatement {
        entity: "PerfAuditUser".to_string(),
        table_alias: None,
        projection: SqlProjection::Items(vec![
            SqlSelectItem::Field("id".to_string()),
            SqlSelectItem::Field("name".to_string()),
        ]),
        projection_aliases: vec![None, None],
        predicate: None,
        distinct: false,
        group_by: vec![],
        having: vec![],
        order_by: vec![SqlOrderTerm {
            field: sql_order_expr("LOWER(name)"),
            direction: SqlOrderDirection::Asc,
        }],
        limit: Some(3),
        offset: None,
    };

    assert!(statement.is_already_local_canonical());
}

#[test]
fn local_grouped_select_with_local_aggregate_is_already_local_canonical() {
    let statement = SqlSelectStatement {
        entity: "PerfAuditUser".to_string(),
        table_alias: None,
        projection: SqlProjection::Items(vec![
            SqlSelectItem::Field("age".to_string()),
            SqlSelectItem::Aggregate(SqlAggregateCall {
                kind: crate::db::sql::parser::SqlAggregateKind::Count,
                input: None,
                filter_expr: None,
                distinct: false,
            }),
        ]),
        projection_aliases: vec![None, None],
        predicate: None,
        distinct: false,
        group_by: vec!["age".to_string()],
        having: vec![],
        order_by: vec![SqlOrderTerm {
            field: sql_order_expr("age"),
            direction: SqlOrderDirection::Asc,
        }],
        limit: Some(10),
        offset: None,
    };

    assert!(statement.is_already_local_canonical());
}

#[test]
fn qualified_field_select_is_not_already_local_canonical() {
    let statement = SqlSelectStatement {
        entity: "public.PerfAuditUser".to_string(),
        table_alias: None,
        projection: SqlProjection::Items(vec![SqlSelectItem::Field(
            "PerfAuditUser.id".to_string(),
        )]),
        projection_aliases: vec![None],
        predicate: Some(SqlExpr::Binary {
            op: SqlExprBinaryOp::Eq,
            left: Box::new(SqlExpr::Field("PerfAuditUser.age".to_string())),
            right: Box::new(SqlExpr::Literal(Value::Int64(24))),
        }),
        distinct: false,
        group_by: vec![],
        having: vec![],
        order_by: vec![SqlOrderTerm {
            field: sql_order_expr("PerfAuditUser.id"),
            direction: SqlOrderDirection::Asc,
        }],
        limit: Some(1),
        offset: None,
    };

    assert!(!statement.is_already_local_canonical());
}

#[test]
fn predicate_identifier_normalization_preserves_nested_field_paths() {
    let statement = SqlSelectStatement {
        entity: "users".to_string(),
        table_alias: Some("u".to_string()),
        projection: SqlProjection::All,
        projection_aliases: vec![],
        predicate: Some(SqlExpr::Binary {
            op: SqlExprBinaryOp::And,
            left: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Eq,
                left: Box::new(SqlExpr::Field("profile.rank".to_string())),
                right: Box::new(SqlExpr::Literal(Value::Int64(5))),
            }),
            right: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Eq,
                left: Box::new(SqlExpr::Field("u.age".to_string())),
                right: Box::new(SqlExpr::Literal(Value::Int64(21))),
            }),
        }),
        distinct: false,
        group_by: vec![],
        having: vec![],
        order_by: vec![],
        limit: None,
        offset: None,
    };

    let normalized = super::normalize_select_statement_to_expected_entity(statement, "users")
        .expect("predicate identifiers should normalize");

    assert_eq!(
        normalized.predicate,
        Some(SqlExpr::Binary {
            op: SqlExprBinaryOp::And,
            left: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Eq,
                left: Box::new(SqlExpr::FieldPath {
                    root: "profile".to_string(),
                    segments: vec!["rank".to_string()],
                }),
                right: Box::new(SqlExpr::Literal(Value::Int64(5))),
            }),
            right: Box::new(SqlExpr::Binary {
                op: SqlExprBinaryOp::Eq,
                left: Box::new(SqlExpr::Field("age".to_string())),
                right: Box::new(SqlExpr::Literal(Value::Int64(21))),
            }),
        }),
    );
}