qail-core 1.2.0

AST-native query builder - type-safe expressions, zero SQL strings
Documentation
use crate::ast::*;
use crate::parser::parse;

#[test]
fn test_parse_call_command() {
    let cmd = parse("call refresh_materialized_views()").unwrap();
    assert_eq!(cmd.action, Action::Call);
    assert_eq!(cmd.table, "refresh_materialized_views()");
}

#[test]
fn test_parse_call_command_rejects_statement_delimiters() {
    assert!(parse("call refresh_materialized_views(); drop table users").is_err());
    assert!(parse("call 1refresh_materialized_views()").is_err());
}

#[test]
fn test_parse_do_command_with_language() {
    let cmd = parse("do $$ BEGIN RAISE NOTICE 'ok'; END; $$ language plpgsql").unwrap();
    assert_eq!(cmd.action, Action::Do);
    assert_eq!(cmd.table, "plpgsql");
    assert_eq!(
        cmd.payload.as_deref(),
        Some(" BEGIN RAISE NOTICE 'ok'; END; ")
    );
}

#[test]
fn test_parse_preserves_comment_markers_inside_quoted_literals() {
    let cmd = parse(r#"get docs fields id where body = "alpha -- beta /* gamma */""#).unwrap();
    assert_eq!(cmd.action, Action::Get);
    assert_eq!(cmd.table, "docs");
    assert_eq!(
        cmd.cages[0].conditions[0].value,
        Value::String("alpha -- beta /* gamma */".to_string())
    );
}

#[test]
fn test_parse_preserves_comment_markers_inside_triple_quoted_literals() {
    let cmd = parse("get docs fields id where body = '''alpha -- beta /* gamma */'''").unwrap();
    assert_eq!(
        cmd.cages[0].conditions[0].value,
        Value::String("alpha -- beta /* gamma */".to_string())
    );
}

#[test]
fn test_parse_strips_comments_outside_literals() {
    let cmd = parse(
        "get docs -- outside line comment\n\
         fields id /* outside block comment */ where active = true",
    )
    .unwrap();

    assert_eq!(cmd.action, Action::Get);
    assert_eq!(cmd.table, "docs");
    assert_eq!(cmd.columns, vec![Expr::Named("id".to_string())]);
    assert_eq!(
        cmd.cages[0].conditions[0].left,
        Expr::Named("active".to_string())
    );
}

#[test]
fn test_parse_do_preserves_comment_markers_inside_dollar_body() {
    let cmd = parse(
        "do $$ BEGIN RAISE NOTICE '-- not a comment /* still body */'; END; $$ language plpgsql",
    )
    .unwrap();

    assert_eq!(cmd.action, Action::Do);
    assert_eq!(
        cmd.payload.as_deref(),
        Some(" BEGIN RAISE NOTICE '-- not a comment /* still body */'; END; ")
    );
}

#[test]
fn test_parse_session_commands() {
    let set_cmd = parse("session set statement_timeout = '5000'").unwrap();
    assert_eq!(set_cmd.action, Action::SessionSet);
    assert_eq!(set_cmd.table, "statement_timeout");
    assert_eq!(set_cmd.payload.as_deref(), Some("5000"));

    let set_guc_cmd = parse("session set app.current_tenant_id = 'tenant-1'").unwrap();
    assert_eq!(set_guc_cmd.action, Action::SessionSet);
    assert_eq!(set_guc_cmd.table, "app.current_tenant_id");
    assert_eq!(set_guc_cmd.payload.as_deref(), Some("tenant-1"));

    let show_cmd = parse("session show statement_timeout").unwrap();
    assert_eq!(show_cmd.action, Action::SessionShow);
    assert_eq!(show_cmd.table, "statement_timeout");

    let reset_cmd = parse("session reset statement_timeout").unwrap();
    assert_eq!(reset_cmd.action, Action::SessionReset);
    assert_eq!(reset_cmd.table, "statement_timeout");
}

#[test]
fn test_session_setting_keys_reject_malformed_names() {
    assert!(parse("session set app..current_tenant_id = tenant-1").is_err());
    assert!(parse("session set 1app.current_tenant_id = tenant-1").is_err());
    assert!(parse("session set app-current_tenant_id = tenant-1").is_err());
    assert!(parse("session show app..current_tenant_id").is_err());
    assert!(parse("session reset app-current_tenant_id").is_err());
}

fn first_op(query: &str) -> Operator {
    let cmd = parse(query).unwrap();
    cmd.cages[0].conditions[0].op
}

#[test]
fn test_parse_extended_symbol_operators() {
    assert_eq!(
        first_op("get users fields id where name ~* \"^a\""),
        Operator::RegexI
    );
    assert_eq!(
        first_op("get users fields id where metadata @> '{\"role\":\"admin\"}'"),
        Operator::Contains
    );
    assert_eq!(
        first_op("get users fields id where tags <@ '[\"a\",\"b\"]'"),
        Operator::ContainedBy
    );
    assert_eq!(
        first_op("get users fields id where tags && '[\"a\"]'"),
        Operator::Overlaps
    );
    assert_eq!(
        first_op("get docs fields id where tsv @@ \"rust\""),
        Operator::TextSearch
    );
    assert_eq!(
        first_op("get docs fields id where data #>> \"{a,b}\""),
        Operator::JsonPathText
    );
    assert_eq!(
        first_op("get docs fields id where data #> \"{a,b}\""),
        Operator::JsonPath
    );
    assert_eq!(
        first_op("get docs fields id where data ? \"key\""),
        Operator::KeyExists
    );
    assert_eq!(
        first_op("get docs fields id where data ?| \"{a,b}\""),
        Operator::KeyExistsAny
    );
    assert_eq!(
        first_op("get docs fields id where data ?& \"{a,b}\""),
        Operator::KeyExistsAll
    );
}

#[test]
fn test_parse_extended_keyword_operators() {
    assert_eq!(
        first_op("get users fields id where name similar to \"(A|B)%\""),
        Operator::SimilarTo
    );
    assert_eq!(
        first_op("get docs fields id where payload json_exists \"$.a\""),
        Operator::JsonExists
    );
    assert_eq!(
        first_op("get docs fields id where payload json_query \"$.a\""),
        Operator::JsonQuery
    );
    assert_eq!(
        first_op("get docs fields id where payload json_value \"$.a\""),
        Operator::JsonValue
    );
    assert_eq!(
        first_op("get users fields id where name regex \"^a\""),
        Operator::Regex
    );
}

#[test]
fn test_bracket_literal_does_not_trigger_table_filter_desugar() {
    let cmd = parse("get users fields id where tags && '[\"a\",\"b\"]'").unwrap();
    assert_eq!(cmd.action, Action::Get);
    assert_eq!(cmd.table, "users");
    assert_eq!(cmd.cages[0].conditions[0].op, Operator::Overlaps);
}