Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/pgls_pretty_print/src/nodes/alter_function_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ pub(super) fn emit_alter_function_stmt(e: &mut EventEmitter, n: &AlterFunctionSt
};

// Emit actions (function options like IMMUTABLE, SECURITY DEFINER, etc.)
// Sort according to Postgres's canonical order
if !n.actions.is_empty() {
e.line(LineType::SoftOrSpace);
emit_comma_separated_list(e, &n.actions, |node, e| {
let sorted_actions = super::create_function_stmt::sort_function_options(&n.actions);
emit_comma_separated_list(e, &sorted_actions, |node, e| {
let def_elem = assert_node_variant!(DefElem, node);
super::create_function_stmt::format_function_option(e, def_elem, dollar_hint);
});
Expand Down
52 changes: 50 additions & 2 deletions crates/pgls_pretty_print/src/nodes/create_function_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ pub(super) fn emit_create_function_stmt(e: &mut EventEmitter, n: &CreateFunction
super::DollarQuoteHint::Function
};

// Options
for option in &n.options {
// Options - sort according to Postgres's canonical order
let sorted_options = sort_function_options(&n.options);
for option in sorted_options {
if let Some(pgls_query::NodeEnum::DefElem(def_elem)) = &option.node {
e.line(LineType::Hard);
format_function_option(e, def_elem, dollar_hint);
Expand Down Expand Up @@ -183,6 +184,53 @@ fn emit_function_parameter_list(e: &mut EventEmitter, params: &[&FunctionParamet
}
}

/// Returns the canonical order for a function option.
/// Postgres's canonical order (as seen in pg_dump output) is:
/// 1. LANGUAGE
/// 2. WINDOW
/// 3. IMMUTABLE / STABLE / VOLATILE (volatility)
/// 4. LEAKPROOF / NOT LEAKPROOF
/// 5. STRICT / CALLED ON NULL INPUT (strict)
/// 6. SECURITY DEFINER / SECURITY INVOKER (security)
/// 7. PARALLEL (parallel)
/// 8. COST (cost)
/// 9. ROWS (rows)
/// 10. SUPPORT (support)
/// 11. SET options (set)
/// 12. AS (function body)
fn option_order(defname: &str) -> usize {
match defname.to_lowercase().as_str() {
"language" => 0,
"window" => 1,
"volatility" => 2,
"leakproof" => 3,
"strict" => 4,
"security" => 5,
"parallel" => 6,
"cost" => 7,
"rows" => 8,
"support" => 9,
"set" => 10,
"as" => 11,
_ => 100, // Unknown options go last
}
}

/// Sort function options according to Postgres's canonical order.
pub(super) fn sort_function_options(
options: &[pgls_query::protobuf::Node],
) -> Vec<pgls_query::protobuf::Node> {
let mut sorted: Vec<pgls_query::protobuf::Node> = options.to_vec();
sorted.sort_by_key(|node| {
if let Some(pgls_query::NodeEnum::DefElem(def_elem)) = &node.node {
option_order(&def_elem.defname)
} else {
100 // Non-DefElem nodes go last
}
});
sorted
}

pub(super) fn format_function_option(
e: &mut EventEmitter,
d: &pgls_query::protobuf::DefElem,
Expand Down
53 changes: 53 additions & 0 deletions crates/pgls_pretty_print/src/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1205,14 +1205,63 @@ fn sql_value_to_func_call(
})
}

/// Returns the canonical order for a function option.
/// Postgres's canonical order (as seen in pg_dump output) is:
/// 1. LANGUAGE
/// 2. WINDOW
/// 3. IMMUTABLE / STABLE / VOLATILE (volatility)
/// 4. LEAKPROOF / NOT LEAKPROOF
/// 5. STRICT / CALLED ON NULL INPUT (strict)
/// 6. SECURITY DEFINER / SECURITY INVOKER (security)
/// 7. PARALLEL (parallel)
/// 8. COST (cost)
/// 9. ROWS (rows)
/// 10. SUPPORT (support)
/// 11. SET options (set)
/// 12. AS (function body)
fn option_order(defname: &str) -> usize {
match defname.to_lowercase().as_str() {
"language" => 0,
"window" => 1,
"volatility" => 2,
"leakproof" => 3,
"strict" => 4,
"security" => 5,
"parallel" => 6,
"cost" => 7,
"rows" => 8,
"support" => 9,
"set" => 10,
"as" => 11,
_ => 100, // Unknown options go last
}
}

/// Sort function options according to Postgres's canonical order.
fn sort_function_options(options: &mut [pgls_query::protobuf::Node]) {
options.sort_by_key(|node| {
if let Some(NodeEnum::DefElem(def_elem)) = &node.node {
option_order(&def_elem.defname)
} else {
100 // Non-DefElem nodes go last
}
});
}

/// Normalize function body strings by trimming whitespace.
///
/// The formatter emits function bodies on separate lines from the dollar-quote delimiters,
/// which adds leading/trailing newlines to the body. This normalization trims these
/// so that semantically equivalent bodies compare equal.
///
/// Also sorts function options to canonical order so that semantically equivalent
/// option orderings compare equal.
fn normalize_function_body(node: &mut NodeEnum) {
match node {
NodeEnum::CreateFunctionStmt(stmt) => {
// Sort options to canonical order
sort_function_options(&mut stmt.options);

for opt in &mut stmt.options {
if let Some(NodeEnum::DefElem(def)) = opt.node.as_mut()
&& def.defname.eq_ignore_ascii_case("as")
Expand Down Expand Up @@ -1242,6 +1291,10 @@ fn normalize_function_body(node: &mut NodeEnum) {
NodeEnum::InlineCodeBlock(block) => {
block.source_text = block.source_text.trim().to_string();
}
NodeEnum::AlterFunctionStmt(stmt) => {
// Sort actions to canonical order
sort_function_options(&mut stmt.actions);
}
_ => {}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,7 @@ create type avg_state as (total bigint, count bigint);

create or replace function avg_transfn(state avg_state, n int)
returns avg_state
language plpgsql
as $function$
declare new_state avg_state;
begin
Expand All @@ -1810,11 +1811,11 @@ begin

return null;
end
$function$
language plpgsql;
$function$;

create function avg_finalfn(state avg_state)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -1823,11 +1824,11 @@ begin
return state.total / state.count;
end if;
end
$function$
language plpgsql;
$function$;

create function sum_finalfn(state avg_state)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -1836,8 +1837,7 @@ begin
return state.total;
end if;
end
$function$
language plpgsql;
$function$;

create aggregate my_avg (int) (stype = avg_state, sfunc = avg_transfn, finalfunc = avg_finalfn);

Expand Down Expand Up @@ -1924,6 +1924,7 @@ begin;

create or replace function sum_transfn(state int, n int)
returns int
language plpgsql
as $function$
declare new_state int4;
begin
Expand All @@ -1941,11 +1942,11 @@ begin

return null;
end
$function$
language plpgsql;
$function$;

create function halfsum_finalfn(state int)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -1954,8 +1955,7 @@ begin
return state / 2;
end if;
end
$function$
language plpgsql;
$function$;

create aggregate my_sum (int) (stype = int, sfunc = sum_transfn);

Expand All @@ -1969,8 +1969,8 @@ begin;

create function balkifnull(bigint, int)
returns bigint
strict
language plpgsql
strict
as $function$
BEGIN
IF $1 IS NULL THEN
Expand Down Expand Up @@ -2136,9 +2136,9 @@ begin;

create function balkifnull(bigint, bigint)
returns bigint
parallel SAFE
strict
language plpgsql
strict
parallel SAFE
as $function$
BEGIN
IF $1 IS NULL THEN
Expand Down Expand Up @@ -2186,8 +2186,8 @@ $function$;
create function rwagg_finalfunc(x anyarray)
returns anyarray
language plpgsql
strict
immutable
strict
as $function$
DECLARE
res x%TYPE;
Expand All @@ -2209,8 +2209,8 @@ create aggregate rwagg (
create function eatarray(x real[])
returns real[]
language plpgsql
strict
immutable
strict
as $function$
BEGIN
x[1] := x[1] + 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2151,6 +2151,7 @@ create type avg_state as (total bigint, count bigint);

create or replace function avg_transfn(state avg_state, n int)
returns avg_state
language plpgsql
as $function$
declare new_state avg_state;
begin
Expand All @@ -2170,11 +2171,11 @@ begin

return null;
end
$function$
language plpgsql;
$function$;

create function avg_finalfn(state avg_state)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -2183,11 +2184,11 @@ begin
return state.total / state.count;
end if;
end
$function$
language plpgsql;
$function$;

create function sum_finalfn(state avg_state)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -2196,8 +2197,7 @@ begin
return state.total;
end if;
end
$function$
language plpgsql;
$function$;

create aggregate my_avg (
int
Expand Down Expand Up @@ -2316,6 +2316,7 @@ begin;

create or replace function sum_transfn(state int, n int)
returns int
language plpgsql
as $function$
declare new_state int4;
begin
Expand All @@ -2333,11 +2334,11 @@ begin

return null;
end
$function$
language plpgsql;
$function$;

create function halfsum_finalfn(state int)
returns int
language plpgsql
as $function$
begin
if state is null then
Expand All @@ -2346,8 +2347,7 @@ begin
return state / 2;
end if;
end
$function$
language plpgsql;
$function$;

create aggregate my_sum (int) (stype = int, sfunc = sum_transfn);

Expand All @@ -2371,8 +2371,8 @@ begin;

create function balkifnull(bigint, int)
returns bigint
strict
language plpgsql
strict
as $function$
BEGIN
IF $1 IS NULL THEN
Expand Down Expand Up @@ -2558,9 +2558,9 @@ begin;

create function balkifnull(bigint, bigint)
returns bigint
parallel SAFE
strict
language plpgsql
strict
parallel SAFE
as $function$
BEGIN
IF $1 IS NULL THEN
Expand Down Expand Up @@ -2608,8 +2608,8 @@ $function$;
create function rwagg_finalfunc(x anyarray)
returns anyarray
language plpgsql
strict
immutable
strict
as $function$
DECLARE
res x%TYPE;
Expand All @@ -2631,8 +2631,8 @@ create aggregate rwagg (
create function eatarray(x real[])
returns real[]
language plpgsql
strict
immutable
strict
as $function$
BEGIN
x[1] := x[1] + 1;
Expand Down
Loading
Loading