uqa-sql 0.2.1

PostgreSQL-compatible SQL compiler built on libpg_query
Documentation
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

use super::*;
use crate::ast::{ColumnType, FromClause, JoinKind, OrderBy, Projection, TableKeyConstraintKind};

#[test]
fn bundled_parser_is_postgresql_18_4() {
    let parsed = pg_query::parse("SELECT 1").expect("parser accepts a scalar query");
    assert_eq!(parsed.protobuf.version, 180_004);
}

#[test]
fn regobject_input_parsers_preserve_postgresql_name_and_type_identity() {
    assert_eq!(
        parse_regobject_name("  Select . \"Mixed.Name\" . a-b  "),
        Some(vec!["select".into(), "Mixed.Name".into(), "a-b".into()])
    );
    assert_eq!(
        parse_regobject_name("\"a\"\"b\""),
        Some(vec!["a\"b".into()])
    );
    assert_eq!(parse_regobject_name("a b"), None);
    assert_eq!(parse_regobject_name("a."), None);
    assert_eq!(
        parse_regobject_name(&"A".repeat(64)),
        Some(vec!["a".repeat(63)])
    );
    assert_eq!(
        parse_regobject_name(&format!("{}é", "A".repeat(62))),
        Some(vec!["a".repeat(62)])
    );

    assert_eq!(
        parse_regtype_name("integer[][]").unwrap(),
        Some(ParsedRegtypeName {
            names: vec!["pg_catalog".into(), "int4".into()],
            array_dimensions: 2,
        })
    );
    assert_eq!(
        parse_regtype_name("\"integer\"").unwrap(),
        Some(ParsedRegtypeName {
            names: vec!["integer".into()],
            array_dimensions: 0,
        })
    );
    assert_eq!(
        parse_regtype_name("integer ARRAY").unwrap(),
        Some(ParsedRegtypeName {
            names: vec!["pg_catalog".into(), "int4".into()],
            array_dimensions: 1,
        })
    );
    assert_eq!(parse_regtype_name("SETOF integer").unwrap(), None);
    assert!(parse_regtype_name("integer, text").is_err());

    let parsed = parse_regprocedure_name("app.\"Mixed\"(integer, \"integer\"[], varchar(10))")
        .unwrap()
        .unwrap();
    assert_eq!(parsed.names, ["app", "Mixed"]);
    assert_eq!(
        parsed.argument_types,
        Some(vec![
            ParsedRegtypeName {
                names: vec!["pg_catalog".into(), "int4".into()],
                array_dimensions: 0,
            },
            ParsedRegtypeName {
                names: vec!["integer".into()],
                array_dimensions: 1,
            },
            ParsedRegtypeName {
                names: vec!["pg_catalog".into(), "varchar".into()],
                array_dimensions: 0,
            },
        ])
    );
    assert_eq!(
        parse_regprocedure_name("app.f").unwrap().unwrap(),
        ParsedRegprocedureName {
            names: vec!["app".into(), "f".into()],
            argument_types: None,
        }
    );
    assert_eq!(
        parse_regprocedure_name("app.f()").unwrap().unwrap(),
        ParsedRegprocedureName {
            names: vec!["app".into(), "f".into()],
            argument_types: Some(Vec::new()),
        }
    );
    assert_eq!(
        parse_regprocedure_name("app.select(integer)")
            .unwrap()
            .unwrap()
            .names,
        ["app", "select"]
    );
}

#[test]
fn transaction_control_preserves_postgresql_modes_and_chaining() {
    use crate::ast::{TransactionCharacteristics, TransactionIsolationLevel, TransactionStmt};

    assert!(matches!(
        first("BEGIN ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE"),
        Statement::Transaction(TransactionStmt::BeginWithCharacteristics(
            TransactionCharacteristics {
                isolation: Some(TransactionIsolationLevel::Serializable),
                read_only: Some(true),
                deferrable: Some(true),
            }
        ))
    ));
    assert!(matches!(
        first("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ WRITE, NOT DEFERRABLE"),
        Statement::Transaction(TransactionStmt::SetCharacteristics(
            TransactionCharacteristics {
                isolation: Some(TransactionIsolationLevel::RepeatableRead),
                read_only: Some(false),
                deferrable: Some(false),
            }
        ))
    ));
    assert!(matches!(
        first("SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY"),
        Statement::Transaction(TransactionStmt::SetSessionCharacteristics(
            TransactionCharacteristics {
                isolation: None,
                read_only: Some(true),
                deferrable: None,
            }
        ))
    ));
    assert!(matches!(
        first("COMMIT AND CHAIN"),
        Statement::Transaction(TransactionStmt::CommitAndChain)
    ));
    assert!(matches!(
        first("ROLLBACK AND CHAIN"),
        Statement::Transaction(TransactionStmt::RollbackAndChain)
    ));
    assert!(matches!(
        first("SET TRANSACTION SNAPSHOT 'FFF-FFF-F'"),
        Statement::Transaction(TransactionStmt::SetSnapshot(ref snapshot))
            if snapshot == "FFF-FFF-F"
    ));
}

#[test]
fn asynchronous_notification_statements_preserve_channels_and_payloads() {
    assert!(matches!(
        first("NOTIFY events, 'ready'"),
        Statement::Notify {
            ref channel,
            ref payload,
        } if channel == "events" && payload == "ready"
    ));
    assert!(matches!(
        first("LISTEN \"Mixed Channel\""),
        Statement::Listen { ref channel } if channel == "Mixed Channel"
    ));
    assert!(matches!(
        first("UNLISTEN \"Mixed Channel\""),
        Statement::Unlisten { channel: Some(ref channel) } if channel == "Mixed Channel"
    ));
    assert!(matches!(
        first("UNLISTEN *"),
        Statement::Unlisten { channel: None }
    ));
    assert!(matches!(
        first("UNLISTEN \"*\""),
        Statement::Unlisten { channel: Some(ref channel) } if channel == "*"
    ));
}

#[test]
fn conditional_notify_rule_action_uses_postgresql_error() {
    let error = compile(
        "CREATE RULE conditional_notify AS ON INSERT TO items WHERE NEW.id > 0 DO ALSO NOTIFY events, 'ready'",
    )
    .expect_err("conditional NOTIFY action must fail");
    assert_eq!(error.sqlstate(), Some("42P17"));
    assert_eq!(
        error.to_string(),
        "rules with WHERE conditions can only have SELECT, INSERT, UPDATE, or DELETE actions"
    );
}

#[test]
fn reset_runtime_parameters_remains_distinct_from_empty_set() {
    assert!(matches!(
        first("RESET default_transaction_read_only"),
        Statement::ResetVariable { ref name }
            if name == "default_transaction_read_only"
    ));
    assert!(matches!(first("RESET ALL"), Statement::ResetAllVariables));
}

#[test]
fn sql_cursor_statements_preserve_postgresql_options_and_fetch_direction() {
    use crate::ast::{CursorDirection, DeclareCursorStmt, FetchCursorStmt};

    let Statement::DeclareCursor(DeclareCursorStmt {
        name,
        binary,
        scroll,
        hold,
        query,
    }) = first("DECLARE \"CaseCursor\" BINARY SCROLL CURSOR WITH HOLD FOR SELECT 1 AS value")
    else {
        panic!("expected DECLARE CURSOR");
    };
    assert_eq!(name, "CaseCursor");
    assert!(binary);
    assert_eq!(scroll, Some(true));
    assert!(hold);
    assert_eq!(query.projections[0].alias.as_deref(), Some("value"));

    assert!(matches!(
        first("FETCH BACKWARD 5 FROM CaseCursor"),
        Statement::FetchCursor(FetchCursorStmt {
            ref name,
            direction: CursorDirection::Backward,
            count: 5,
            move_only: false,
        }) if name == "casecursor"
    ));
    assert!(matches!(
        first("MOVE ABSOLUTE -1 IN CaseCursor"),
        Statement::FetchCursor(FetchCursorStmt {
            direction: CursorDirection::Absolute,
            count: -1,
            move_only: true,
            ..
        })
    ));
    assert!(matches!(
        first("FETCH ALL FROM CaseCursor"),
        Statement::FetchCursor(FetchCursorStmt {
            direction: CursorDirection::Forward,
            count: i64::MAX,
            ..
        })
    ));
    assert!(matches!(
        first("CLOSE CaseCursor"),
        Statement::CloseCursor { name: Some(ref name) } if name == "casecursor"
    ));
    assert!(matches!(
        first("CLOSE ALL"),
        Statement::CloseCursor { name: None }
    ));
}

#[test]
fn function_arguments_reject_positional_after_named_and_duplicate_names() {
    let error = compile("SELECT random(max => 2, 1)").unwrap_err();
    assert_eq!(error.sqlstate(), Some("42601"));
    assert_eq!(
        error.to_string(),
        "positional argument cannot follow named argument"
    );

    let error = compile("SELECT random(min => 1, min => 2)").unwrap_err();
    assert_eq!(error.sqlstate(), Some("42601"));
    assert_eq!(
        error.to_string(),
        "argument name \"min\" used more than once"
    );
}

#[test]
fn returning_row_aliases_preserve_quoted_identifier_case() {
    let Statement::Insert(insert) = first(
        "INSERT INTO items VALUES (1) RETURNING WITH (OLD AS \"Image\", NEW AS \"image\") \"Image\".*, \"image\".*",
    ) else {
        panic!("expected INSERT");
    };
    assert_eq!(insert.returning_aliases.old, "Image");
    assert_eq!(insert.returning_aliases.new, "image");
    assert!(insert.returning_aliases.old_explicit);
    assert!(insert.returning_aliases.new_explicit);

    let error =
        compile("INSERT INTO items VALUES (1) RETURNING WITH (OLD AS image, NEW AS IMAGE) image.*")
            .unwrap_err();
    assert_eq!(error.sqlstate(), Some("42712"));
    assert!(error
        .to_string()
        .contains("table name \"image\" specified more than once"));
}

#[test]
fn syntax_calls_preserve_polymorphic_builtin_identity() {
    let Statement::Select(select) = first(
        "SELECT coalesce(1, 2), greatest(1, 2), least(1, 2), nullif(1, 2),
                \"coalesce\"(1, 2), ordinary.coalesce(1, 2)",
    ) else {
        panic!("expected SELECT");
    };
    for (projection, expected_name) in select.projections[..4]
        .iter()
        .zip(["coalesce", "greatest", "least", "nullif"])
    {
        let Expr::Func {
            name,
            binding: Some(binding),
            ..
        } = &projection.expr
        else {
            panic!("expected syntax call for {expected_name}");
        };
        assert_eq!(name, expected_name);
        assert_eq!(binding.name, expected_name);
        assert!(binding.builtin);
        assert!(binding.argument_types.is_empty());
    }
    for (projection, expected_name) in select.projections[4..]
        .iter()
        .zip(["\"coalesce\"", "ordinary.coalesce"])
    {
        assert!(matches!(
            &projection.expr,
            Expr::Func {
                name,
                binding: None,
                ..
            } if name == expected_name
        ));
    }
}

fn first(sql: &str) -> Statement {
    let mut v = compile(sql).unwrap();
    assert_eq!(v.len(), 1, "expected 1 stmt");
    v.remove(0)
}

fn null_literal_node() -> Node {
    Node {
        node: Some(NodeEnum::AConst(pg_query::protobuf::AConst {
            isnull: true,
            ..Default::default()
        })),
    }
}

mod compiler_invariants;
mod data_commands;
mod ddl_lifecycle;
mod grouping;
mod query_features;
mod relations;
mod routines;
mod row_locking;
mod schema_definitions;