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;