Skip to content

Commit 14ccc0e

Browse files
authored
Add utils/datum.h bindings + generic_agg example (#2341)
## Motivation These are the canonical, type-length-aware way to copy/compare a `Datum` when the concrete type is only known at runtime (`anyelement`, polymorphic aggregates, generic state). Today an extension author has to hand-redeclare them `extern "C"` — easy to get subtly wrong (`typLen` is C `int`, not `int16`) and missing pgrx's `#[pg_guard]`, so a `palloc` OOM longjmp becomes UB. ## What this does - Add `#include "utils/datum.h"` to all seven wrappers (`pg13`–`pg19`). - Add `pgrx-examples/generic_agg`: a polymorphic `count_changes(anyelement)` aggregate that needs `datumCopy` (keep a by-ref value alive across transition calls) + `datumIsEqual` (byte-compare type-erased values), with `#[pg_test]`s covering text/numeric (by-ref), int4 (by-value), NULL, and empty input.
1 parent 63c08c1 commit 14ccc0e

12 files changed

Lines changed: 287 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2+
#LICENSE
3+
#LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4+
#LICENSE
5+
#LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6+
#LICENSE
7+
#LICENSE All rights reserved.
8+
#LICENSE
9+
#LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10+
11+
[package]
12+
name = "generic_agg"
13+
version = "0.0.0"
14+
edition.workspace = true
15+
rust-version.workspace = true
16+
publish = false
17+
18+
[lib]
19+
crate-type = ["cdylib"]
20+
21+
[features]
22+
default = ["pg13"]
23+
pg13 = ["pgrx/pg13", "pgrx-tests/pg13"]
24+
pg14 = ["pgrx/pg14", "pgrx-tests/pg14"]
25+
pg15 = ["pgrx/pg15", "pgrx-tests/pg15"]
26+
pg16 = ["pgrx/pg16", "pgrx-tests/pg16"]
27+
pg17 = ["pgrx/pg17", "pgrx-tests/pg17"]
28+
pg18 = ["pgrx/pg18", "pgrx-tests/pg18"]
29+
pg19 = ["pgrx/pg19", "pgrx-tests/pg19"]
30+
pg_test = []
31+
32+
[dependencies]
33+
pgrx = { path = "../../pgrx" }
34+
35+
[dev-dependencies]
36+
pgrx-tests = { path = "../../pgrx-tests" }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# generic_agg
2+
3+
A worked example proving why pgrx needs the `utils/datum.h` bindings, It implements one polymorphic aggregate:
4+
5+
```sql
6+
count_changes(anyelement) -> bigint
7+
```
8+
9+
which counts how many times the input value differs from the previous row.
10+
11+
## Run the tests
12+
13+
```bash
14+
cargo pgrx test pg18 -p generic_agg
15+
```
16+
17+
The tests cover pass-by-reference (`text`, `numeric`), pass-by-value (`int4`), NULL handling, and the empty-input case.
18+
19+
## Try it
20+
21+
```sql
22+
SELECT count_changes(v ORDER BY ord)
23+
FROM (VALUES (1,'a'),(2,'a'),(3,'b'),(4,'b'),(5,'b'),(6,'c')) t(ord, v);
24+
-- 2
25+
*/
26+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
comment = 'generic_agg: polymorphic aggregate demonstrating utils/datum.h'
2+
default_version = '@CARGO_VERSION@'
3+
module_pathname = 'generic_agg'
4+
relocatable = false
5+
superuser = false
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
//LICENSE Portions Copyright 2019-2021 ZomboDB, LLC.
2+
//LICENSE
3+
//LICENSE Portions Copyright 2021-2023 Technology Concepts & Design, Inc.
4+
//LICENSE
5+
//LICENSE Portions Copyright 2023-2023 PgCentral Foundation, Inc. <contact@pgcentral.org>
6+
//LICENSE
7+
//LICENSE All rights reserved.
8+
//LICENSE
9+
//LICENSE Use of this source code is governed by the MIT license that can be found in the LICENSE file.
10+
11+
//! `generic_agg` — a worked example that proves why pgrx needs `utils/datum.h`.
12+
//!
13+
//! It implements a polymorphic aggregate:
14+
//!
15+
//! ```sql
16+
//! count_changes(anyelement) -> bigint
17+
//! ```
18+
//!
19+
//! which counts how many times the input value differs from the previous row
20+
//! (a classic run-length / change-detection aggregate).
21+
//!
22+
//! The aggregate receives a raw `pg_sys::Datum` whose concrete type is only
23+
//! known at runtime (`anyelement`). To carry "the value seen so far" forward
24+
//! across transition calls, it must:
25+
//!
26+
//! * `datumCopy` — deep-copy a (possibly pass-by-reference) value into the
27+
//! long-lived aggregate memory context, so it survives the per-tuple context
28+
//! being recycled before the next row, and
29+
//! * `datumIsEqual` — byte-compare two type-erased values to decide whether the
30+
//! value changed.
31+
//!
32+
//! Both functions are declared only in `utils/datum.h`. Without that header an
33+
//! extension author would have to hand-redeclare them `extern "C"`.
34+
35+
use pgrx::aggregate::*;
36+
use pgrx::prelude::*;
37+
use pgrx::{AnyElement, Internal};
38+
39+
pgrx::pg_module_magic!(name, version);
40+
41+
/// Per-group transition state, allocated in the aggregate's memory context.
42+
///
43+
/// `prev` holds a **copy** (made with `datumCopy`) of the most-recently-seen value, so it stays valid across transition calls even for pass-by-reference types whose original Datum lives in the short-lived per-tuple context.
44+
#[derive(Clone, Copy)]
45+
struct ChangeState {
46+
changes: i64,
47+
typlen: i16,
48+
typbyval: bool,
49+
type_known: bool,
50+
has_prev: bool,
51+
prev: pg_sys::Datum,
52+
}
53+
54+
/// Marker type naming the SQL aggregate `count_changes`.
55+
#[derive(AggregateName)]
56+
#[aggregate_name = "count_changes"]
57+
struct CountChanges;
58+
59+
#[pg_aggregate]
60+
impl Aggregate<CountChanges> for CountChanges {
61+
type State = Internal;
62+
type Args = Option<AnyElement>;
63+
type Finalize = i64;
64+
65+
fn state(
66+
mut current: Self::State,
67+
arg: Self::Args,
68+
fcinfo: pg_sys::FunctionCallInfo,
69+
) -> Self::State {
70+
// NULL inputs aren't a "change"; leave the state untouched.
71+
let elem = match arg {
72+
Some(e) => e,
73+
None => return current,
74+
};
75+
76+
unsafe {
77+
// Transition functions run with CurrentMemoryContext set to a *short-lived* per-tuple context. Anything we need on the next row (the state struct AND the copied Datum) must instead be allocated n the long-lived aggregate context obtained here.
78+
let mut agg_ctx: pg_sys::MemoryContext = core::ptr::null_mut();
79+
if pg_sys::AggCheckCallContext(fcinfo, &mut agg_ctx) == 0 {
80+
error!("count_changes can only be used as an aggregate");
81+
}
82+
let old_ctx = pg_sys::MemoryContextSwitchTo(agg_ctx);
83+
84+
let st = current.get_or_insert_with(|| ChangeState {
85+
changes: 0,
86+
typlen: 0,
87+
typbyval: false,
88+
type_known: false,
89+
has_prev: false,
90+
prev: pg_sys::Datum::null(),
91+
});
92+
93+
// Resolve (typlen, typbyval) once from the runtime element type.
94+
if !st.type_known {
95+
pg_sys::get_typlenbyval(elem.oid(), &mut st.typlen, &mut st.typbyval);
96+
st.type_known = true;
97+
}
98+
99+
let cur = elem.datum();
100+
// datumCopy/datumIsEqual take C `int` for typlen.
101+
let typlen = st.typlen as i32;
102+
103+
if st.has_prev {
104+
// datumIsEqual is a *byte-level* comparison.
105+
if !pg_sys::datumIsEqual(st.prev, cur, st.typbyval, typlen) {
106+
st.changes += 1;
107+
// Release the previous copy (no-op for pass-by-value types).
108+
if !st.typbyval {
109+
pg_sys::pfree(st.prev.cast_mut_ptr());
110+
}
111+
st.prev = pg_sys::datumCopy(cur, st.typbyval, typlen);
112+
}
113+
} else {
114+
st.prev = pg_sys::datumCopy(cur, st.typbyval, typlen);
115+
st.has_prev = true;
116+
}
117+
118+
pg_sys::MemoryContextSwitchTo(old_ctx);
119+
}
120+
121+
current
122+
}
123+
124+
fn finalize(
125+
current: Self::State,
126+
_direct_args: Self::OrderedSetArgs,
127+
_fcinfo: pg_sys::FunctionCallInfo,
128+
) -> Self::Finalize {
129+
// No rows (or all-NULL input) => zero changes.
130+
unsafe { current.get::<ChangeState>().map(|s| s.changes).unwrap_or(0) }
131+
}
132+
}
133+
134+
#[cfg(any(test, feature = "pg_test"))]
135+
#[pg_schema]
136+
mod tests {
137+
#[allow(unused_imports)]
138+
use pgrx::prelude::*;
139+
140+
// text is pass-by-reference: exercises the datumCopy/datumIsEqual path.
141+
// Ordered a,a,b,b,b,c => 2.
142+
#[pg_test]
143+
fn count_changes_text() {
144+
let changes = Spi::get_one::<i64>(
145+
"SELECT count_changes(v ORDER BY ord) FROM \
146+
(VALUES (1,'a'),(2,'a'),(3,'b'),(4,'b'),(5,'b'),(6,'c')) t(ord, v)",
147+
)
148+
.expect("SPI result was NULL");
149+
assert_eq!(changes, Some(2));
150+
}
151+
152+
// numeric exercises the varlena path (typlen = -1).
153+
// Ordered 1.5,1.5,1.5,2.0,2.0,3.0,1.5 => 3.
154+
#[pg_test]
155+
fn count_changes_numeric() {
156+
let changes = Spi::get_one::<i64>(
157+
"SELECT count_changes(v ORDER BY ord) FROM (VALUES \
158+
(1,1.5::numeric),(2,1.5),(3,1.5),(4,2.0),(5,2.0),(6,3.0),(7,1.5)) t(ord, v)",
159+
)
160+
.expect("SPI result was NULL");
161+
assert_eq!(changes, Some(3));
162+
}
163+
164+
// int4 is pass-by-value: proves the no-op-copy branch and byte-compare.
165+
// Ordered 1,1,2,2,2,1,1 => 2.
166+
#[pg_test]
167+
fn count_changes_int4() {
168+
let changes = Spi::get_one::<i64>(
169+
"SELECT count_changes(v ORDER BY ord) FROM \
170+
(VALUES (1,1),(2,1),(3,2),(4,2),(5,2),(6,1),(7,1)) t(ord, v)",
171+
)
172+
.expect("SPI result was NULL");
173+
assert_eq!(changes, Some(2));
174+
}
175+
176+
// NULLs are ignored, not counted as changes.
177+
// Ordered non-null subsequence a,a,b,b => 1.
178+
#[pg_test]
179+
fn count_changes_ignores_nulls() {
180+
let changes = Spi::get_one::<i64>(
181+
"SELECT count_changes(v ORDER BY ord) FROM (VALUES \
182+
(1,'a'),(2,NULL),(3,'a'),(4,'b'),(5,NULL),(6,'b')) t(ord, v)",
183+
)
184+
.expect("SPI result was NULL");
185+
assert_eq!(changes, Some(1));
186+
}
187+
188+
// No rows => 0.
189+
#[pg_test]
190+
fn count_changes_empty() {
191+
let changes =
192+
Spi::get_one::<i64>("SELECT count_changes(v) FROM (SELECT 1 WHERE false) t(v)")
193+
.expect("SPI result was NULL");
194+
assert_eq!(changes, Some(0));
195+
}
196+
}
197+
198+
#[cfg(test)]
199+
pub mod pg_test {
200+
pub fn setup(_options: Vec<&str>) {}
201+
202+
pub fn postgresql_conf_options() -> Vec<&'static str> {
203+
vec![]
204+
}
205+
}

pgrx-pg-sys/include/pg13.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@
184184
#include "utils/builtins.h"
185185
#include "utils/date.h"
186186
#include "utils/datetime.h"
187+
#include "utils/datum.h"
187188
#include "utils/elog.h"
188189
#include "utils/float.h"
189190
#include "utils/fmgroids.h"

pgrx-pg-sys/include/pg14.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
#include "utils/builtins.h"
186186
#include "utils/date.h"
187187
#include "utils/datetime.h"
188+
#include "utils/datum.h"
188189
#include "utils/elog.h"
189190
#include "utils/float.h"
190191
#include "utils/fmgroids.h"

pgrx-pg-sys/include/pg15.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@
186186
#include "utils/builtins.h"
187187
#include "utils/date.h"
188188
#include "utils/datetime.h"
189+
#include "utils/datum.h"
189190
#include "utils/elog.h"
190191
#include "utils/float.h"
191192
#include "utils/fmgroids.h"

pgrx-pg-sys/include/pg16.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@
188188
#include "utils/builtins.h"
189189
#include "utils/date.h"
190190
#include "utils/datetime.h"
191+
#include "utils/datum.h"
191192
#include "utils/elog.h"
192193
#include "utils/float.h"
193194
#include "utils/fmgroids.h"

pgrx-pg-sys/include/pg17.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@
189189
#include "utils/builtins.h"
190190
#include "utils/date.h"
191191
#include "utils/datetime.h"
192+
#include "utils/datum.h"
192193
#include "utils/elog.h"
193194
#include "utils/float.h"
194195
#include "utils/fmgroids.h"

0 commit comments

Comments
 (0)