From 291842d956740af67d7a19ae5e8508fd811504c3 Mon Sep 17 00:00:00 2001 From: SaltyDavidselph <46937183+SaltyDavidselph@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:34:42 -0600 Subject: [PATCH] Test reorg (#358) The new `plrust-tests` create is a standalone pgrx crate that only contains the unit tests from `plrust`. These were copied over verbatim, followed by minor organizational and formatting changes. To run the tests: ```shell $ cd plrust-tests $ ./run-tests.sh pgXX [test_name] ``` Co-authored-by: Eric B. Ridge Co-authored-by: Brady Bonnette Co-authored-by: Jubilee Young --- .github/workflows/ci.yml | 55 +- Cargo.lock | 56 +- Cargo.toml | 1 + plrust-tests/Cargo.toml | 25 + plrust-tests/plrust_tests.control | 6 + plrust-tests/run-tests.sh | 30 + plrust-tests/src/alter.rs | 42 + plrust-tests/src/argument.rs | 71 ++ plrust-tests/src/basic.rs | 123 +++ plrust-tests/src/blocked_code.rs | 182 ++++ plrust-tests/src/borrow_mut_error.rs | 56 + plrust-tests/src/ddl.rs | 92 ++ plrust-tests/src/dependencies.rs | 102 ++ plrust-tests/src/lib.rs | 108 ++ plrust-tests/src/matches.rs | 114 ++ plrust-tests/src/panics.rs | 62 ++ plrust-tests/src/range.rs | 53 + plrust-tests/src/recursion.rs | 12 + plrust-tests/src/return_values.rs | 82 ++ plrust-tests/src/round_trip.rs | 95 ++ plrust-tests/src/time_and_dates.rs | 84 ++ plrust-tests/src/trusted.rs | 240 ++++ plrust-tests/src/user_defined_types.rs | 98 ++ plrust-tests/src/versioning.rs | 53 + plrust/src/tests.rs | 1390 +----------------------- 25 files changed, 1814 insertions(+), 1418 deletions(-) create mode 100644 plrust-tests/Cargo.toml create mode 100644 plrust-tests/plrust_tests.control create mode 100755 plrust-tests/run-tests.sh create mode 100644 plrust-tests/src/alter.rs create mode 100644 plrust-tests/src/argument.rs create mode 100644 plrust-tests/src/basic.rs create mode 100644 plrust-tests/src/blocked_code.rs create mode 100644 plrust-tests/src/borrow_mut_error.rs create mode 100644 plrust-tests/src/ddl.rs create mode 100644 plrust-tests/src/dependencies.rs create mode 100644 plrust-tests/src/lib.rs create mode 100644 plrust-tests/src/matches.rs create mode 100644 plrust-tests/src/panics.rs create mode 100644 plrust-tests/src/range.rs create mode 100644 plrust-tests/src/recursion.rs create mode 100644 plrust-tests/src/return_values.rs create mode 100644 plrust-tests/src/round_trip.rs create mode 100644 plrust-tests/src/time_and_dates.rs create mode 100644 plrust-tests/src/trusted.rs create mode 100644 plrust-tests/src/user_defined_types.rs create mode 100644 plrust-tests/src/versioning.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fefc4b0..36e275e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,13 +148,29 @@ jobs: - name: Run cargo pgrx init run: cargo pgrx init --pg$PG_VER $(which pg_config) - - name: Test PL/rust as "untrusted" - if: matrix.target == 'host' - run: cargo test --all --features "pg$PG_VER" --no-default-features + - name: Install PL/Rust as "trusted" + if: matrix.target == 'postgrestd' + run: cd plrust && STD_TARGETS="aarch64-postgres-linux-gnu" ./build && echo "\q" | cargo pgrx run "pg$PG_VER" --features "trusted" + + - name: Test PL/Rust package as "trusted" + if: matrix.target == 'postgrestd' + run: cd plrust && cargo test --no-default-features --features "pg$PG_VER trusted" - - name: Test PL/rust as "trusted" (inc. postgrestd) + - name: Run PL/Rust integration tests as "trusted" if: matrix.target == 'postgrestd' - run: cd plrust && STD_TARGETS="aarch64-postgres-linux-gnu" ./build && cargo test --verbose --no-default-features --features "pg$PG_VER trusted" + run: cd plrust && echo "\q" | cargo pgrx run "pg$PG_VER" --features "trusted" && cd ../plrust-tests && cargo test --no-default-features --features "pg$PG_VER trusted" + + - name: Install PL/Rust as "untrusted" + if: matrix.target == 'host' + run: cd plrust && STD_TARGETS="aarch64-postgres-linux-gnu" ./build && echo "\q" | cargo pgrx run "pg$PG_VER" + + - name: Test PL/Rust package as "untrusted" + if: matrix.target == 'host' + run: cd plrust && cargo test --no-default-features --features "pg$PG_VER" + + - name: Run PL/Rust integration tests as "untrusted" + if: matrix.target == 'host' + run: cd plrust && echo "\q" | cargo pgrx run "pg$PG_VER" && cd ../plrust-tests && cargo test --no-default-features --features "pg$PG_VER" - name: Print sccache stats (after build) run: sccache --show-stats @@ -315,6 +331,9 @@ jobs: - name: Print sccache stats run: sccache --show-stats + - name: Test plrustc + run: cd plrustc && cargo test + - name: Install plrustc run: cd plrustc && ./build.sh && cp ../build/bin/plrustc ~/.cargo/bin @@ -324,13 +343,29 @@ jobs: - name: Run 'cargo pgrx init' against system-level ${{ matrix.version }} run: cargo pgrx init --pg$PG_VER $(which pg_config) - - name: Test PL/rust as "untrusted" - if: matrix.target == 'host' - run: cargo test --all --features "pg$PG_VER" --no-default-features + - name: Install PL/Rust as "trusted" + if: matrix.target == 'postgrestd' + run: cd plrust && STD_TARGETS="x86_64-postgres-linux-gnu" ./build && echo "\q" | cargo pgrx run "pg$PG_VER" --features "trusted" - - name: Test PL/rust as "trusted" (inc. postgrestd) + - name: Test PL/Rust package as "trusted" if: matrix.target == 'postgrestd' - run: cd plrust && STD_TARGETS="x86_64-postgres-linux-gnu" ./build && cargo test --verbose --no-default-features --features "pg$PG_VER trusted" + run: cd plrust && cargo test --no-default-features --features "pg$PG_VER trusted" + + - name: Run PL/Rust integration tests as "trusted" + if: matrix.target == 'postgrestd' + run: cd plrust && echo "\q" | cargo pgrx run "pg$PG_VER" --features "trusted" && cd ../plrust-tests && cargo test --no-default-features --features "pg$PG_VER trusted" + + - name: Install PL/Rust as "untrusted" + if: matrix.target == 'host' + run: cd plrust && STD_TARGETS="x86_64-postgres-linux-gnu" ./build && echo "\q" | cargo pgrx run "pg$PG_VER" + + - name: Test PL/Rust package as "untrusted" + if: matrix.target == 'host' + run: cd plrust && cargo test --no-default-features --features "pg$PG_VER" + + - name: Run PL/Rust integration tests as "untrusted" + if: matrix.target == 'host' + run: cd plrust && echo "\q" | cargo pgrx run "pg$PG_VER" && cd ../plrust-tests && cargo test --no-default-features --features "pg$PG_VER" - name: Print sccache stats run: sccache --show-stats diff --git a/Cargo.lock b/Cargo.lock index c027a948..5b9aa62a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,12 +479,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "fixedbitset" @@ -710,15 +707,6 @@ dependencies = [ "hashbrown 0.14.0", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-lifetimes" version = "1.0.11" @@ -780,6 +768,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + [[package]] name = "lock_api" version = "0.4.10" @@ -826,7 +820,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffc89ccdc6e10d6907450f753537ebc5c5d3460d2e4e62ea74bd571db62c0f9e" dependencies = [ - "rustix", + "rustix 0.37.23", ] [[package]] @@ -1182,6 +1176,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "plrust-tests" +version = "0.0.0" +dependencies = [ + "once_cell", + "pgrx", + "pgrx-tests", + "tempfile", +] + [[package]] name = "plrust-trusted-pgrx" version = "1.2.3" @@ -1437,7 +1441,20 @@ dependencies = [ "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys", +] + +[[package]] +name = "rustix" +version = "0.38.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.5", "windows-sys", ] @@ -1688,15 +1705,14 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", - "rustix", + "rustix 0.38.8", "windows-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 688c9e95..4bcb26a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "plrust", "plrust-trusted-pgrx", + "plrust-tests", ] exclude = ["plrustc"]#, "builder"] diff --git a/plrust-tests/Cargo.toml b/plrust-tests/Cargo.toml new file mode 100644 index 00000000..b32a49f1 --- /dev/null +++ b/plrust-tests/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "plrust-tests" +version = "0.0.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[features] +default = ["pg13"] +pg13 = ["pgrx/pg13", "pgrx-tests/pg13" ] +pg14 = ["pgrx/pg14", "pgrx-tests/pg14" ] +pg15 = ["pgrx/pg15", "pgrx-tests/pg15" ] +pg_test = [] +trusted = [] + +[dependencies] +pgrx = "=0.9.7" +tempfile = "3.8.0" +once_cell = "1.18.0" + +[dev-dependencies] +pgrx-tests = "=0.9.7" +tempfile = "3.8.0" +once_cell = "1.18.0" diff --git a/plrust-tests/plrust_tests.control b/plrust-tests/plrust_tests.control new file mode 100644 index 00000000..48a73b33 --- /dev/null +++ b/plrust-tests/plrust_tests.control @@ -0,0 +1,6 @@ +comment = 'plrust_tests: Created by pgrx' +default_version = '@CARGO_VERSION@' +module_pathname = '$libdir/plrust_tests' +relocatable = false +superuser = true +requires = 'plrust' diff --git a/plrust-tests/run-tests.sh b/plrust-tests/run-tests.sh new file mode 100755 index 00000000..a90e6067 --- /dev/null +++ b/plrust-tests/run-tests.sh @@ -0,0 +1,30 @@ +#! /bin/bash + +VERSION=$1 + +if [ -z ${VERSION} ]; then + echo "usage: ./run-tests.sh pgXX [test-name]" + exit 1 +fi + +TEST_DIR=`pwd` + +set -e + +# install the plrust extension into the pgrx-managed postgres +echo "============================" +echo " installing plrust" +echo +cd ../plrust +echo "\q" | cargo pgrx run ${VERSION} + +# run the test suite from this crate +cd ${TEST_DIR} + +echo +echo "============================" +echo " running plrust-tests suite" +echo + +cargo pgrx test ${VERSION} $2 + diff --git a/plrust-tests/src/alter.rs b/plrust-tests/src/alter.rs new file mode 100644 index 00000000..78216270 --- /dev/null +++ b/plrust-tests/src/alter.rs @@ -0,0 +1,42 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "plrust functions cannot have their STRICT property altered"] + fn plrust_cant_change_strict_off() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION cant_change_strict_off() + RETURNS int + LANGUAGE plrust + AS $$ Ok(Some(1)) $$; + "#; + Spi::run(definition)?; + Spi::run("ALTER FUNCTION cant_change_strict_off() CALLED ON NULL INPUT") + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "plrust functions cannot have their STRICT property altered"] + fn plrust_cant_change_strict_on() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION cant_change_strict_on() + RETURNS int + LANGUAGE plrust + AS $$ Ok(Some(1)) $$; + "#; + Spi::run(definition)?; + Spi::run("ALTER FUNCTION cant_change_strict_on() RETURNS NULL ON NULL INPUT") + } +} diff --git a/plrust-tests/src/argument.rs b/plrust-tests/src/argument.rs new file mode 100644 index 00000000..1bc2a270 --- /dev/null +++ b/plrust-tests/src/argument.rs @@ -0,0 +1,71 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "parameter name \"a\" used more than once"] + fn plrust_dup_args() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION not_unique(a int, a int) + RETURNS int AS + $$ + Ok(a) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result = Spi::get_one::("SELECT not_unique(1, 2);\n"); + assert_eq!(Ok(Some(1)), result); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "PL/Rust does not support unnamed arguments"] + fn plrust_defaulting_dup_args() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION not_unique(int, arg0 int) + RETURNS int AS + $$ + Ok(arg0) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result = Spi::get_one::("SELECT not_unique(1, 2);\n"); + assert_eq!(Ok(Some(1)), result); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "PL/Rust does not support unnamed arguments")] + fn unnamed_args() -> spi::Result<()> { + Spi::run("CREATE FUNCTION unnamed_arg(int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "PL/Rust does not support unnamed arguments")] + fn named_unnamed_args() -> spi::Result<()> { + Spi::run("CREATE FUNCTION named_unnamed_arg(bob text, int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic( + expected = "is an invalid Rust identifier and cannot be used as an argument name" + )] + fn invalid_arg_identifier() -> spi::Result<()> { + Spi::run("CREATE FUNCTION invalid_arg_identifier(\"this isn't a valid rust identifier\" int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") + } +} diff --git a/plrust-tests/src/basic.rs b/plrust-tests/src/basic.rs new file mode 100644 index 00000000..10e882d7 --- /dev/null +++ b/plrust-tests/src/basic.rs @@ -0,0 +1,123 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::{datum::IntoDatum, prelude::*}; + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_basic() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION sum_array(a BIGINT[]) RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + Ok(Some(a.into_iter().map(|v| v.unwrap_or_default()).sum())) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one_with_args::( + r#" + SELECT sum_array($1); + "#, + vec![( + PgBuiltInOids::INT4ARRAYOID.oid(), + vec![1, 2, 3].into_datum(), + )], + ); + assert_eq!(retval, Ok(Some(6))); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_update() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION update_me() RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + Ok(String::from("booper").into()) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one( + r#" + SELECT update_me(); + "#, + ); + assert_eq!(retval, Ok(Some("booper"))); + + let definition = r#" + CREATE OR REPLACE FUNCTION update_me() RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + Ok(Some(String::from("swooper"))) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one( + r#" + SELECT update_me(); + "#, + ); + assert_eq!(retval, Ok(Some("swooper"))); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_spi() -> spi::Result<()> { + let random_definition = r#" + CREATE FUNCTION random_contributor_pet() RETURNS TEXT + STRICT + LANGUAGE PLRUST AS + $$ + Ok(Spi::get_one("SELECT name FROM contributors_pets ORDER BY random() LIMIT 1")?) + $$; + "#; + Spi::run(random_definition)?; + + let retval = Spi::get_one::( + r#" + SELECT random_contributor_pet(); + "#, + ); + assert!(retval.is_ok()); + assert!(retval.unwrap().is_some()); + + let specific_definition = r#" + CREATE FUNCTION contributor_pet(name TEXT) RETURNS BIGINT + STRICT + LANGUAGE PLRUST AS + $$ + use pgrx::IntoDatum; + Ok(Spi::get_one_with_args( + "SELECT id FROM contributors_pets WHERE name = $1", + vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())], + )?) + $$; + "#; + Spi::run(specific_definition)?; + + let retval = Spi::get_one::( + r#" + SELECT contributor_pet('Nami'); + "#, + ); + assert_eq!(retval, Ok(Some(2))); + Ok(()) + } +} diff --git a/plrust-tests/src/blocked_code.rs b/plrust-tests/src/blocked_code.rs new file mode 100644 index 00000000..3376dfe6 --- /dev/null +++ b/plrust-tests/src/blocked_code.rs @@ -0,0 +1,182 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@ extschema @)] + #[should_panic = "error: usage of an `unsafe` block"] + fn plrust_block_unsafe_annotated() -> spi::Result<()> { + // PL/Rust should block creating obvious, correctly-annotated usage of unsafe code + let definition = r#" + CREATE FUNCTION naughty() + RETURNS text AS + $$ + use std::{os::raw as ffi, str, ffi::CStr}; + let int:u32 = 0xDEADBEEF; + // Note that it is always safe to create a pointer. + let ptr = int as *mut u64; + // What is unsafe is dereferencing it + let cstr = unsafe { + ptr.write(0x00_1BADC0DE_00); + CStr::from_ptr(ptr.cast::()) + }; + Ok(str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition) + } + + #[pg_test] + #[search_path(@ extschema @)] + #[should_panic = "call to unsafe function is unsafe and requires unsafe block"] + fn plrust_block_unsafe_hidden() -> spi::Result<()> { + // PL/Rust should not allow hidden injection of unsafe code + // that may rely on the way PGRX expands into `unsafe fn` to "sneak in" + let definition = r#" + CREATE FUNCTION naughty() + RETURNS text AS + $$ + use std::{os::raw as ffi, str, ffi::CStr}; + let int:u32 = 0xDEADBEEF; + let ptr = int as *mut u64; + ptr.write(0x00_1BADC0DE_00); + let cstr = CStr::from_ptr(ptr.cast::()); + Ok(str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "error: usage of an `unsafe` block"] + fn plrust_block_unsafe_plutonium() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION super_safe() + RETURNS text AS + $$ + [dependencies] + plutonium = "*" + + [code] + use std::{os::raw as ffi, str, ffi::CStr}; + use plutonium::safe; + + #[safe] + fn super_safe() -> Option { + let int: u32 = 0xDEADBEEF; + let ptr = int as *mut u64; + ptr.write(0x00_1BADC0DE_00); + let cstr = CStr::from_ptr(ptr.cast::()); + str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned()) + } + + Ok(super_safe()) + $$ LANGUAGE plrust; + "#; + Spi::run(definition) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "error: declaration of a function with `export_name`")] + fn plrust_block_unsafe_export_name() -> spi::Result<()> { + // A separate test covers #[no_mangle], but what about #[export_name]? + // Same idea. This tries to collide with free, which may symbol clash, + // or might override depending on how the OS and loader feel today. + // Let's not leave it up to forces beyond our control. + let definition = r#" + CREATE OR REPLACE FUNCTION export_hacked_free() RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + #[export_name = "free"] + pub extern "C" fn own_free(ptr: *mut c_void) { + // the contents don't matter + } + + Ok(Some(1)) + $$; + "#; + Spi::run(definition)?; + let result = Spi::get_one::("SELECT export_hacked_free();\n"); + assert_eq!(Ok(Some(1)), result); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "error: declaration of a static with `link_section`")] + fn plrust_block_unsafe_link_section() -> spi::Result<()> { + let definition = r#" + CREATE OR REPLACE FUNCTION link_evil_section() RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + #[link_section = ".init_array"] + pub static INITIALIZE: &[u8; 136] = &GOGO; + + #[link_section = ".text"] + pub static GOGO: [u8; 136] = [ + 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, + 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, + 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, + 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, + 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, + 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, + 106, 59, 88, 15, 5, + ]; + + Ok(Some(1)) + $$; + "#; + Spi::run(definition)?; + let result = Spi::get_one::("SELECT link_evil_section();\n"); + assert_eq!(Ok(Some(1)), result); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic(expected = "error: declaration of a `no_mangle` static")] + fn plrust_block_unsafe_no_mangle() -> spi::Result<()> { + let definition = r#" + CREATE OR REPLACE FUNCTION not_mangled() RETURNS BIGINT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + #[no_mangle] + #[link_section = ".init_array"] + pub static INITIALIZE: &[u8; 136] = &GOGO; + + #[no_mangle] + #[link_section = ".text"] + pub static GOGO: [u8; 136] = [ + 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, + 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, + 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, + 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, + 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, + 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, + 106, 59, 88, 15, 5, + ]; + + Ok(Some(1)) + $$; + "#; + Spi::run(definition)?; + let result = Spi::get_one::("SELECT not_mangled();\n"); + assert_eq!(Ok(Some(1)), result); + Ok(()) + } +} diff --git a/plrust-tests/src/borrow_mut_error.rs b/plrust-tests/src/borrow_mut_error.rs new file mode 100644 index 00000000..d78162f9 --- /dev/null +++ b/plrust-tests/src/borrow_mut_error.rs @@ -0,0 +1,56 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[should_panic(expected = "issue78 works")] + fn test_issue_78() -> spi::Result<()> { + let sql = r#"CREATE OR REPLACE FUNCTION raise_error() RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + pgrx::error!("issue78 works"); + Ok(Some("hi".to_string())) + $$;"#; + Spi::run(sql)?; + Spi::get_one::("SELECT raise_error()")?; + Ok(()) + } + + #[pg_test] + fn test_issue_79() -> spi::Result<()> { + let sql = r#" + create or replace function fn1(i int) returns int strict language plrust as $$ + [code] + notice!("{}", "fn1 started"); + let cmd = format!("select fn2({})", i); + Spi::connect(|client| + { + client.select(&cmd, None, None); + }); + notice!("{}", "fn1 finished"); + Ok(Some(1)) + $$; + + create or replace function fn2(i int) returns int strict language plrust as $$ + [code] + notice!("{}", "fn2 started"); + notice!("{}", "fn2 finished"); + Ok(Some(2)) + $$; + "#; + Spi::run(sql)?; + assert_eq!(Ok(Some(1)), Spi::get_one::("SELECT fn1(1)")); + Ok(()) + } +} diff --git a/plrust-tests/src/ddl.rs b/plrust-tests/src/ddl.rs new file mode 100644 index 00000000..44fb1383 --- /dev/null +++ b/plrust-tests/src/ddl.rs @@ -0,0 +1,92 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_aggregate() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION plrust_sum_state(state INT, next INT) RETURNS INT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + Ok(Some(state + next)) + $$; + CREATE AGGREGATE plrust_sum(INT) + ( + SFUNC = plrust_sum_state, + STYPE = INT, + INITCOND = '0' + ); + "#; + Spi::run(definition)?; + + let retval = Spi::get_one::( + r#" + SELECT plrust_sum(value) FROM UNNEST(ARRAY [1, 2, 3]) as value; + "#, + ); + assert_eq!(retval, Ok(Some(6))); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_trigger() -> spi::Result<()> { + let definition = r#" + CREATE TABLE dogs ( + name TEXT, + scritches INT NOT NULL DEFAULT 0 + ); + + CREATE FUNCTION pet_trigger() RETURNS trigger AS $$ + let mut new = trigger.new().unwrap().into_owned(); + + let field = "scritches"; + + match new.get_by_name::(field)? { + Some(val) => new.set_by_name(field, val + 1)?, + None => (), + } + + Ok(Some(new)) + $$ LANGUAGE plrust; + + CREATE TRIGGER pet_trigger BEFORE INSERT OR UPDATE ON dogs + FOR EACH ROW EXECUTE FUNCTION pet_trigger(); + + INSERT INTO dogs (name) VALUES ('Nami'); + "#; + Spi::run(definition)?; + + let retval = Spi::get_one::( + r#" + SELECT scritches FROM dogs; + "#, + ); + assert_eq!(retval, Ok(Some(1))); + Ok(()) + } + + #[pg_test] + fn replace_function() -> spi::Result<()> { + Spi::run("CREATE FUNCTION replace_me() RETURNS int LANGUAGE plrust AS $$ Ok(Some(1)) $$")?; + assert_eq!(Ok(Some(1)), Spi::get_one("SELECT replace_me()")); + + Spi::run( + "CREATE OR REPLACE FUNCTION replace_me() RETURNS int LANGUAGE plrust AS $$ Ok(Some(2)) $$", + )?; + assert_eq!(Ok(Some(2)), Spi::get_one("SELECT replace_me()")); + Ok(()) + } +} diff --git a/plrust-tests/src/dependencies.rs b/plrust-tests/src/dependencies.rs new file mode 100644 index 00000000..08a20180 --- /dev/null +++ b/plrust-tests/src/dependencies.rs @@ -0,0 +1,102 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[cfg(not(feature = "sandboxed"))] + #[search_path(@extschema@)] + fn plrust_deps_supported() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION colorize(input TEXT) RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + [dependencies] + owo-colors = "3" + [code] + use owo_colors::OwoColorize; + Ok(Some(input.purple().to_string())) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one_with_args::( + r#" + SELECT colorize($1); + "#, + vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], + ); + assert!(retval.is_ok()); + assert!(retval.unwrap().is_some()); + + // Regression test: A previous version of PL/Rust would abort if this was called twice, so call it twice: + let retval = Spi::get_one_with_args::( + r#" + SELECT colorize($1); + "#, + vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], + ); + assert!(retval.is_ok()); + assert!(retval.unwrap().is_some()); + Ok(()) + } + + #[pg_test] + #[cfg(not(feature = "sandboxed"))] + #[search_path(@extschema@)] + fn plrust_deps_supported_deps_in_toml_table() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION say_hello() RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + [dependencies] + tokio = ">=1" + owo-colors = "3" + [code] + Ok(Some("hello".to_string())) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one_with_args::( + r#" + SELECT say_hello(); + "#, + vec![(PgBuiltInOids::TEXTOID.oid(), "hello".into_datum())], + ); + assert_eq!(retval, Ok(Some("hello".to_string()))); + Ok(()) + } + + #[pg_test] + #[cfg(not(feature = "sandboxed"))] + #[search_path(@extschema@)] + fn plrust_deps_not_supported() { + let definition = r#" + CREATE FUNCTION colorize(input TEXT) RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + [dependencies] + regex = "1.6.5" + [code] + Ok(Some("test")) + $$; + "#; + let res = std::panic::catch_unwind(|| { + Spi::run(definition).expect("SQL for plrust_deps_not_supported() failed") + }); + assert!(res.is_err()); + } +} diff --git a/plrust-tests/src/lib.rs b/plrust-tests/src/lib.rs new file mode 100644 index 00000000..ca066415 --- /dev/null +++ b/plrust-tests/src/lib.rs @@ -0,0 +1,108 @@ +mod alter; +mod argument; +mod basic; +mod blocked_code; +mod borrow_mut_error; +mod ddl; +mod dependencies; +mod matches; +mod panics; +mod range; +mod recursion; +mod return_values; +mod round_trip; +mod time_and_dates; +mod trusted; +mod user_defined_types; +mod versioning; + +use pgrx::prelude::*; + +pgrx::pg_module_magic!(); + +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + // Bootstrap a testing table for non-immutable functions + extension_sql!( + r#" + CREATE TABLE contributors_pets ( + id serial8 not null primary key, + name text + ); + INSERT INTO contributors_pets (name) VALUES ('Brandy'); + INSERT INTO contributors_pets (name) VALUES ('Nami'); + INSERT INTO contributors_pets (name) VALUES ('Sally'); + INSERT INTO contributors_pets (name) VALUES ('Anchovy'); + "#, + name = "create_contributors_pets", + ); +} + +#[cfg(any(test, feature = "pg_test"))] +pub mod pg_test { + use once_cell::sync::Lazy; + use tempfile::{tempdir, TempDir}; + static WORK_DIR: Lazy = Lazy::new(|| { + let work_dir = tempdir().expect("Couldn't create tempdir"); + format!("plrust.work_dir='{}'", work_dir.path().display()) + }); + static LOG_LEVEL: &str = "plrust.tracing_level=trace"; + static PLRUST_ALLOWED_DEPENDENCIES_FILE_NAME: &str = "allowed_deps.toml"; + static PLRUST_ALLOWED_DEPENDENCIES_FILE_DIRECTORY: Lazy = Lazy::new(|| { + use std::io::Write; + let temp_allowed_deps_dir = tempdir().expect("Couldnt create tempdir"); + + let file_path = temp_allowed_deps_dir + .path() + .join(PLRUST_ALLOWED_DEPENDENCIES_FILE_NAME); + let mut allowed_deps = std::fs::File::create(&file_path).unwrap(); + allowed_deps + .write_all( + r#" +owo-colors = "=3.5.0" +tokio = { version = "=1.19.2", features = ["rt", "net"]} +plutonium = "*" +"# + .as_bytes(), + ) + .unwrap(); + + temp_allowed_deps_dir + }); + + static PLRUST_ALLOWED_DEPENDENCIES: Lazy = Lazy::new(|| { + format!( + "plrust.allowed_dependencies='{}'", + PLRUST_ALLOWED_DEPENDENCIES_FILE_DIRECTORY + .path() + .join(PLRUST_ALLOWED_DEPENDENCIES_FILE_NAME) + .to_str() + .unwrap() + ) + }); + + pub fn setup(_options: Vec<&str>) { + // perform one-off initialization when the pg_test framework starts + } + + pub fn postgresql_conf_options() -> Vec<&'static str> { + vec![ + &*WORK_DIR, + &*LOG_LEVEL, + &*PLRUST_ALLOWED_DEPENDENCIES, + "shared_preload_libraries='plrust'", + ] + } +} diff --git a/plrust-tests/src/matches.rs b/plrust-tests/src/matches.rs new file mode 100644 index 00000000..13c7b56a --- /dev/null +++ b/plrust-tests/src/matches.rs @@ -0,0 +1,114 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_call_1st() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION ret_1st(a int, b int) + RETURNS int AS + $$ + Ok(a) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result_1 = Spi::get_one::("SELECT ret_1st(1, 2);\n"); + assert_eq!(Ok(Some(1)), result_1); // may get: Some(1) + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_call_2nd() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION ret_2nd(a int, b int) + RETURNS int AS + $$ + Ok(b) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result_2 = Spi::get_one::("SELECT ret_2nd(1, 2);\n"); + assert_eq!(Ok(Some(2)), result_2); // may get: Some(2) + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_call_me() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION pick_ret(a int, b int, pick int) + RETURNS int AS + $$ + Ok(match pick { + Some(0) => a, + Some(1) => b, + _ => None, + }) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result_a = Spi::get_one::("SELECT pick_ret(3, 4, 0);"); + let result_b = Spi::get_one::("SELECT pick_ret(5, 6, 1);"); + let result_c = Spi::get_one::("SELECT pick_ret(7, 8, 2);"); + let result_z = Spi::get_one::("SELECT pick_ret(9, 99, -1);"); + assert_eq!(Ok(Some(3)), result_a); // may get: Some(4) or None + assert_eq!(Ok(Some(6)), result_b); // may get: None + assert_eq!(Ok(None), result_c); + assert_eq!(Ok(None), result_z); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + fn plrust_call_me_call_me() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION ret_1st(a int, b int) + RETURNS int AS + $$ + Ok(a) + $$ LANGUAGE plrust; + + CREATE FUNCTION ret_2nd(a int, b int) + RETURNS int AS + $$ + Ok(b) + $$ LANGUAGE plrust; + + CREATE FUNCTION pick_ret(a int, b int, pick int) + RETURNS int AS + $$ + Ok(match pick { + Some(0) => a, + Some(1) => b, + _ => None, + }) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let result_1 = Spi::get_one::("SELECT ret_1st(1, 2);\n"); + let result_2 = Spi::get_one::("SELECT ret_2nd(1, 2);\n"); + let result_a = Spi::get_one::("SELECT pick_ret(3, 4, 0);"); + let result_b = Spi::get_one::("SELECT pick_ret(5, 6, 1);"); + let result_c = Spi::get_one::("SELECT pick_ret(7, 8, 2);"); + let result_z = Spi::get_one::("SELECT pick_ret(9, 99, -1);"); + assert_eq!(Ok(None), result_z); + assert_eq!(Ok(None), result_c); + assert_eq!(Ok(Some(6)), result_b); // may get: None + assert_eq!(Ok(Some(3)), result_a); // may get: Some(4) or None + assert_eq!(Ok(Some(2)), result_2); // may get: Some(1) + assert_eq!(Ok(Some(1)), result_1); // may get: Some(2) + Ok(()) + } +} diff --git a/plrust-tests/src/panics.rs b/plrust-tests/src/panics.rs new file mode 100644 index 00000000..9db4b48d --- /dev/null +++ b/plrust-tests/src/panics.rs @@ -0,0 +1,62 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "yup"] + fn pgrx_can_panic() { + panic!("yup") + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "yup"] + fn plrust_can_panic() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION shut_up_and_explode() + RETURNS text AS + $$ + panic!("yup"); + Ok(None) + $$ LANGUAGE plrust; + "#; + + Spi::run(definition)?; + let retval = Spi::get_one::("SELECT shut_up_and_explode();\n"); + assert_eq!(retval, Ok(None)); + Ok(()) + } + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "xxx"] + #[ignore] + fn plrust_pgloglevel_dont_allcaps_panic() -> spi::Result<()> { + // This test attempts to annihilate the database. + // It relies on the existing assumption that tests are run in the same Postgres instance, + // so this test will make all tests "flaky" if Postgres suddenly goes down with it. + let definition = r#" + CREATE FUNCTION dont_allcaps_panic() + RETURNS text AS + $$ + ereport!(PANIC, PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, "If other tests completed, PL/Rust did not actually destroy the entire database, \ + But if you see this in the error output, something might be wrong."); + Ok(Some("lol".into())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + let retval = Spi::get_one::("SELECT dont_allcaps_panic();\n"); + assert_eq!(retval, Ok(Some("lol".into()))); + Ok(()) + } +} diff --git a/plrust-tests/src/range.rs b/plrust-tests/src/range.rs new file mode 100644 index 00000000..eacbbb1f --- /dev/null +++ b/plrust-tests/src/range.rs @@ -0,0 +1,53 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + fn test_int4range() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION test_int4range(r int4range) RETURNS int4range LANGUAGE plrust AS $$ Ok(r) $$"#, + )?; + let r = Spi::get_one::>("SELECT test_int4range('[1, 10)'::int4range);")? + .expect("SPI result was null"); + assert_eq!(r, (1..10).into()); + Ok(()) + } + + #[pg_test] + fn test_int8range() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION test_int8range(r int8range) RETURNS int8range LANGUAGE plrust AS $$ Ok(r) $$"#, + )?; + let r = Spi::get_one::>("SELECT test_int8range('[1, 10)'::int8range);")? + .expect("SPI result was null"); + assert_eq!(r, (1..10).into()); + Ok(()) + } + + #[pg_test] + fn test_numrange() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION test_numrange(r numrange) RETURNS numrange LANGUAGE plrust AS $$ Ok(r) $$"#, + )?; + let r = Spi::get_one::>("SELECT test_numrange('[1, 10]'::numrange);")? + .expect("SPI result was null"); + assert_eq!( + r, + Range::new( + AnyNumeric::try_from(1.0f32).unwrap(), + AnyNumeric::try_from(10.0f32).unwrap() + ) + ); + Ok(()) + } +} diff --git a/plrust-tests/src/recursion.rs b/plrust-tests/src/recursion.rs new file mode 100644 index 00000000..12817423 --- /dev/null +++ b/plrust-tests/src/recursion.rs @@ -0,0 +1,12 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests {} diff --git a/plrust-tests/src/return_values.rs b/plrust-tests/src/return_values.rs new file mode 100644 index 00000000..0e4b79a8 --- /dev/null +++ b/plrust-tests/src/return_values.rs @@ -0,0 +1,82 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[search_path(@ extschema @)] + fn plrust_returns_setof() -> spi::Result<()> { + let definition = r#" + CREATE OR REPLACE FUNCTION boop_srf(names TEXT[]) RETURNS SETOF TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + Ok(Some(::pgrx::iter::SetOfIterator::new(names.into_iter().map(|maybe| maybe.map(|name| name.to_string() + " was booped!"))))) + $$; + "#; + Spi::run(definition)?; + + let retval: spi::Result<_> = Spi::connect(|client| { + let mut table = client.select( + "SELECT * FROM boop_srf(ARRAY['Nami', 'Brandy'])", + None, + None, + )?; + + let mut found = vec![]; + while table.next().is_some() { + let value = table.get_one::()?; + found.push(value) + } + + Ok(Some(found)) + }); + + assert_eq!( + retval, + Ok(Some(vec![ + Some("Nami was booped!".into()), + Some("Brandy was booped!".into()), + ])) + ); + Ok(()) + } + + #[pg_test] + fn test_srf_one_col() -> spi::Result<()> { + Spi::run( + "CREATE FUNCTION srf_one_col() RETURNS TABLE (a int) LANGUAGE plrust AS $$ + Ok(Some(TableIterator::new(vec![( Some(1), )].into_iter()))) + $$;", + )?; + + let a = Spi::get_one::("SELECT * FROM srf_one_col()")?; + assert_eq!(a, Some(1)); + + Ok(()) + } + + #[pg_test] + fn test_srf_two_col() -> spi::Result<()> { + Spi::run( + "CREATE FUNCTION srf_two_col() RETURNS TABLE (a int, b int) LANGUAGE plrust AS $$ + Ok(Some(TableIterator::new(vec![(Some(1), Some(2))].into_iter()))) + $$;", + )?; + + let (a, b) = Spi::get_two::("SELECT * FROM srf_two_col()")?; + assert_eq!(a, Some(1)); + assert_eq!(b, Some(2)); + + Ok(()) + } +} diff --git a/plrust-tests/src/round_trip.rs b/plrust-tests/src/round_trip.rs new file mode 100644 index 00000000..dbb2eab4 --- /dev/null +++ b/plrust-tests/src/round_trip.rs @@ -0,0 +1,95 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + use std::error::Error; + + #[pg_test] + fn test_tid_roundtrip() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION tid_roundtrip(t tid) RETURNS tid LANGUAGE plrust AS $$ Ok(t) $$"#, + )?; + let tid = Spi::get_one::("SELECT tid_roundtrip('(42, 99)'::tid)")? + .expect("SPI result was null"); + let (blockno, offno) = pgrx::item_pointer_get_both(tid); + assert_eq!(blockno, 42); + assert_eq!(offno, 99); + Ok(()) + } + + #[pg_test] + fn test_return_bytea() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION return_bytea() RETURNS bytea LANGUAGE plrust AS $$ Ok(Some(vec![1,2,3])) $$"#, + )?; + let bytes = Spi::get_one::>("SELECT return_bytea()")?.expect("SPI result was null"); + assert_eq!(bytes, vec![1, 2, 3]); + Ok(()) + } + + #[pg_test] + fn test_cstring_roundtrip() -> Result<(), Box> { + use std::ffi::CStr; + Spi::run( + r#"CREATE FUNCTION cstring_roundtrip(s cstring) RETURNS cstring STRICT LANGUAGE plrust as $$ Ok(Some(s.into())) $$;"#, + )?; + let cstr = Spi::get_one::<&CStr>("SELECT cstring_roundtrip('hello')")? + .expect("SPI result was null"); + let expected = CStr::from_bytes_with_nul(b"hello\0")?; + assert_eq!(cstr, expected); + Ok(()) + } + + #[pg_test] + fn test_point() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION test_point(p point) RETURNS point LANGUAGE plrust AS $$ Ok(p) $$"#, + )?; + let p = Spi::get_one::("SELECT test_point('42, 99'::point);")? + .expect("SPI result was null"); + assert_eq!(p.x, 42.0); + assert_eq!(p.y, 99.0); + Ok(()) + } + + #[pg_test] + fn test_box() -> spi::Result<()> { + Spi::run(r#"CREATE FUNCTION test_box(b box) RETURNS box LANGUAGE plrust AS $$ Ok(b) $$"#)?; + let b = Spi::get_one::("SELECT test_box('1,2,3,4'::box);")? + .expect("SPI result was null"); + assert_eq!(b.high.x, 3.0); + assert_eq!(b.high.y, 4.0); + assert_eq!(b.low.x, 1.0); + assert_eq!(b.low.y, 2.0); + Ok(()) + } + + #[pg_test] + fn test_uuid() -> spi::Result<()> { + Spi::run( + r#"CREATE FUNCTION test_uuid(u uuid) RETURNS uuid LANGUAGE plrust AS $$ Ok(u) $$"#, + )?; + let u = Spi::get_one::( + "SELECT test_uuid('e4176a4d-790c-4750-85b7-665d72471173'::uuid);", + )? + .expect("SPI result was null"); + assert_eq!( + u, + pgrx::Uuid::from_bytes([ + 0xe4, 0x17, 0x6a, 0x4d, 0x79, 0x0c, 0x47, 0x50, 0x85, 0xb7, 0x66, 0x5d, 0x72, 0x47, + 0x11, 0x73 + ]) + ); + + Ok(()) + } +} diff --git a/plrust-tests/src/time_and_dates.rs b/plrust-tests/src/time_and_dates.rs new file mode 100644 index 00000000..472ae381 --- /dev/null +++ b/plrust-tests/src/time_and_dates.rs @@ -0,0 +1,84 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + use std::error::Error; + + #[pg_test] + fn test_daterange() -> Result<(), Box> { + Spi::run( + r#"CREATE FUNCTION test_daterange(r daterange) RETURNS daterange LANGUAGE plrust AS $$ Ok(r) $$"#, + )?; + let r = Spi::get_one::>( + "SELECT test_daterange('[1977-03-20, 1980-01-01)'::daterange);", + )? + .expect("SPI result was null"); + assert_eq!( + r, + Range::new( + Date::new(1977, 3, 20)?, + RangeBound::Exclusive(Date::new(1980, 01, 01)?) + ) + ); + Ok(()) + } + + #[pg_test] + fn test_tsrange() -> Result<(), Box> { + Spi::run( + r#"CREATE FUNCTION test_tsrange(p tsrange) RETURNS tsrange LANGUAGE plrust AS $$ Ok(p) $$"#, + )?; + let r = Spi::get_one::>( + "SELECT test_tsrange('[1977-03-20, 1980-01-01)'::tsrange);", + )? + .expect("SPI result was null"); + assert_eq!( + r, + Range::new( + Timestamp::new(1977, 3, 20, 0, 0, 0.0)?, + RangeBound::Exclusive(Timestamp::new(1980, 01, 01, 0, 0, 0.0)?) + ) + ); + Ok(()) + } + + #[pg_test] + fn test_tstzrange() -> Result<(), Box> { + Spi::run( + r#"CREATE FUNCTION test_tstzrange(p tstzrange) RETURNS tstzrange LANGUAGE plrust AS $$ Ok(p) $$"#, + )?; + let r = Spi::get_one::>( + "SELECT test_tstzrange('[1977-03-20, 1980-01-01)'::tstzrange);", + )? + .expect("SPI result was null"); + assert_eq!( + r, + Range::new( + TimestampWithTimeZone::new(1977, 3, 20, 0, 0, 0.0)?, + RangeBound::Exclusive(TimestampWithTimeZone::new(1980, 01, 01, 0, 0, 0.0)?) + ) + ); + Ok(()) + } + + #[pg_test] + fn test_interval() -> Result<(), Box> { + Spi::run( + r#"CREATE FUNCTION get_interval_hours(i interval) RETURNS numeric STRICT LANGUAGE plrust AS $$ Ok(i.extract_part(DateTimeParts::Hour)) $$"#, + )?; + let hours = + Spi::get_one::("SELECT get_interval_hours('3 days 9 hours 12 seconds')")? + .expect("SPI result was null"); + assert_eq!(hours, AnyNumeric::from(9)); + Ok(()) + } +} diff --git a/plrust-tests/src/trusted.rs b/plrust-tests/src/trusted.rs new file mode 100644 index 00000000..33b373a6 --- /dev/null +++ b/plrust-tests/src/trusted.rs @@ -0,0 +1,240 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + #[allow(unused)] + use pgrx::prelude::*; + + #[cfg(feature = "trusted")] + #[pg_test] + #[search_path(@extschema@)] + fn postgrestd_dont_make_files() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION make_file(filename TEXT) RETURNS TEXT + LANGUAGE PLRUST AS + $$ + Ok(std::fs::File::create(filename.unwrap_or("/somewhere/files/dont/belong.txt")) + .err() + .map(|e| e.to_string())) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one_with_args::( + r#" + SELECT make_file($1); + "#, + vec![( + PgBuiltInOids::TEXTOID.oid(), + "/an/evil/place/to/put/a/file.txt".into_datum(), + )], + ); + assert_eq!( + retval, + Ok(Some("operation not supported on this platform".to_string())) + ); + Ok(()) + } + + #[cfg(feature = "trusted")] + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "Failed to execute command"] + fn postgrestd_subprocesses_panic() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION say_hello() + RETURNS text AS + $$ + let out = std::process::Command::new("echo") + .arg("Hello world") + .stdout(std::process::Stdio::piped()) + .output() + .expect("Failed to execute command"); + Ok(Some(String::from_utf8_lossy(&out.stdout).to_string())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one::("SELECT say_hello();\n"); + assert_eq!(retval, Ok(Some("Hello world\n".into()))); + Ok(()) + } + + #[cfg(feature = "trusted")] + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "error: the `include_str`, `include_bytes`, and `include` macros are forbidden"] + fn postgrestd_no_include_str() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION include_str() + RETURNS text AS + $$ + let s = include_str!("/etc/passwd"); + Ok(Some(s.into())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one::("SELECT include_str();\n")?; + assert_eq!(retval.unwrap(), ""); + Ok(()) + } + + #[pg_test] + #[search_path(@extschema@)] + #[cfg(feature = "trusted")] + #[should_panic = "No such file or directory (os error 2)"] + fn plrustc_include_exists_no_access() { + // This file is created in CI and exists, but can only be accessed by + // root. Check that the actual access is reported as file not found (we + // should be ensuring that via + // `PLRUSTC_USER_CRATE_ALLOWED_SOURCE_PATHS`). We don't need to gate + // this test on CI, since the file is unlikely to exist outside of CI + // (so the test will pass). + let definition = r#" + CREATE FUNCTION include_no_access() + RETURNS text AS $$ + include!("/var/ci-stuff/secret_rust_files/const_foo.rs"); + Ok(Some(format!("{BAR}"))) + $$ LANGUAGE plrust; + "#; + Spi::run(definition).unwrap() + } + + #[pg_test] + #[search_path(@extschema@)] + #[cfg(feature = "trusted")] + #[should_panic = "No such file or directory (os error 2)"] + fn plrustc_include_exists_external() { + // This file is created in CI, exists, and can be accessed by anybody, + // but the actual access is forbidden via + // `PLRUSTC_USER_CRATE_ALLOWED_SOURCE_PATHS`. We don't need to gate this test on + // CI, since the file is unlikely to exist outside of CI, so the test + // will pass anyway. + let definition = r#" + CREATE FUNCTION include_exists_external() + RETURNS text AS $$ + include!("/var/ci-stuff/const_bar.rs"); + Ok(Some(format!("{BAR}"))) + $$ LANGUAGE plrust; + "#; + Spi::run(definition).unwrap(); + } + + #[pg_test] + #[search_path(@extschema@)] + #[cfg(feature = "trusted")] + #[should_panic = "No such file or directory (os error 2)"] + fn plrustc_include_made_up() { + // This file does not exist, and should be reported as such. + let definition = r#" + CREATE FUNCTION include_madeup() + RETURNS int AS $$ + include!("/made/up/path/lol.rs"); + Ok(Some(1)) + $$ LANGUAGE plrust; + "#; + Spi::run(definition).unwrap(); + } + + #[pg_test] + #[search_path(@extschema@)] + #[cfg(feature = "trusted")] + #[should_panic = "No such file or directory (os error 2)"] + fn plrustc_include_path_traversal() { + use std::path::PathBuf; + let workdir = Spi::get_one::("SHOW plrust.work_dir") + .expect("Could not get plrust.work_dir") + .unwrap(); + + let wd: PathBuf = PathBuf::from(workdir) + .canonicalize() + .ok() + .expect("Failed to canonicalize workdir"); + + // Produce a path that looks like + // `/allowed/path/here/../../../illegal/path/here` and check that it's + // rejected, in order to ensure we are not succeptable to path traversal + // attacks. + let mut evil_path = wd.clone(); + for _ in wd.ancestors().skip(1) { + evil_path.push(".."); + } + debug_assert_eq!( + evil_path + .canonicalize() + .ok() + .expect("Failed to produce unpath") + .to_str(), + Some("/") + ); + evil_path.push("var/ci-stuff/const_bar.rs"); + // This file does not exist, and should be reported as such. + let definition = format!( + r#"CREATE FUNCTION include_sneaky_traversal() + RETURNS int AS $$ + include!({evil_path:?}); + Ok(Some(1)) + $$ LANGUAGE plrust;"# + ); + Spi::run(&definition).unwrap(); + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "error: the `env` and `option_env` macros are forbidden"] + #[cfg(feature = "trusted")] + fn plrust_block_env() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION get_path() RETURNS text AS $$ + let path = env!("PATH"); + Ok(Some(path.to_string())) + $$ LANGUAGE plrust; + "#; + Spi::run(definition) + } + + #[pg_test] + #[search_path(@extschema@)] + #[should_panic = "error: the `env` and `option_env` macros are forbidden"] + #[cfg(feature = "trusted")] + fn plrust_block_option_env() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION try_get_path() RETURNS text AS $$ + match option_env!("PATH") { + None => Ok(None), + Some(s) => Ok(Some(s.to_string())) + } + $$ LANGUAGE plrust; + "#; + Spi::run(definition) + } + + #[cfg(feature = "trusted")] + #[pg_test] + #[search_path(@extschema@)] + fn postgrestd_net_is_unsupported() -> spi::Result<()> { + let sql = r#" + create or replace function pt106() returns text + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + [code] + use std::net::TcpStream; + + Ok(TcpStream::connect("127.0.0.1:22").err().map(|e| e.to_string())) + $$"#; + Spi::run(sql)?; + let string = Spi::get_one::("SELECT pt106()")?.expect("Unconditional return"); + assert_eq!("operation not supported on this platform", &string); + Ok(()) + } +} diff --git a/plrust-tests/src/user_defined_types.rs b/plrust-tests/src/user_defined_types.rs new file mode 100644 index 00000000..18ae7d2f --- /dev/null +++ b/plrust-tests/src/user_defined_types.rs @@ -0,0 +1,98 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + fn test_udt() -> spi::Result<()> { + Spi::run( + r#" +CREATE TYPE person AS ( + name text, + age float8 +); + +create function make_person(name text, age float8) returns person + strict parallel safe + language plrust as +$$ + // create the Heap Tuple representation of the SQL type `person` + let mut p = PgHeapTuple::new_composite_type("person")?; + + // set a few of its attributes + // + // Runtime errors can occur if the attribute name is invalid or if the Rust type of the value + // is not compatible with the backing SQL type for that attribute. Hence the use of the `?` operator + p.set_by_name("name", name)?; + p.set_by_name("age", age)?; + + // return the `person` + Ok(Some(p)) +$$; + +create function get_person_name(p person) returns text + strict parallel safe + language plrust as +$$ + // `p` is a `PgHeapTuple` over the underlying data for `person` + Ok(p.get_by_name("name")?) +$$; + +create function get_person_age(p person) returns float8 + strict parallel safe + language plrust as +$$ + // `p` is a `PgHeapTuple` over the underlying data for `person` + Ok(p.get_by_name("age")?) +$$; + +create function get_person_attribute(p person, attname text) returns text + strict parallel safe + language plrust as +$$ + match attname.to_lowercase().as_str() { + "age" => { + let age:Option = p.get_by_name("age")?; + Ok(age.map(|v| v.to_string())) + }, + "name" => { + Ok(p.get_by_name("name")?) + }, + _ => panic!("unknown attribute: `{attname}`") + } +$$; + +create operator ->> (function = get_person_attribute, leftarg = person, rightarg = text); + +create table people +( + id serial8 not null primary key, + p person +); + +insert into people (p) values (make_person('Johnny', 46.24)); +insert into people (p) values (make_person('Joe', 99.09)); +insert into people (p) values (make_person('Dr. Beverly Crusher of the Starship Enterprise', 32.0)); + "#, + )?; + + let johnny = Spi::get_one::>( + "SELECT p FROM people WHERE p->>'name' = 'Johnny';", + )? + .expect("SPI result was null"); + + let age = johnny.get_by_name::("age")?.expect("age was null"); + assert_eq!(age, 46.24); + + Ok(()) + } +} diff --git a/plrust-tests/src/versioning.rs b/plrust-tests/src/versioning.rs new file mode 100644 index 00000000..69d4147d --- /dev/null +++ b/plrust-tests/src/versioning.rs @@ -0,0 +1,53 @@ +/* +Portions Copyright 2020-2021 ZomboDB, LLC. +Portions Copyright 2021-2023 Technology Concepts & Design, Inc. + +All rights reserved. + +Use of this source code is governed by the PostgreSQL license that can be found in the LICENSE.md file. +*/ + +#[cfg(any(test, feature = "pg_test"))] +#[pgrx::pg_schema] +mod tests { + use pgrx::prelude::*; + + #[pg_test] + #[cfg(not(feature = "sandboxed"))] + #[search_path(@extschema@)] + fn plrust_deps_supported_semver_parse() -> spi::Result<()> { + let definition = r#" + CREATE FUNCTION colorize(input TEXT) RETURNS TEXT + IMMUTABLE STRICT + LANGUAGE PLRUST AS + $$ + [dependencies] + owo-colors = ">2" + [code] + use owo_colors::OwoColorize; + Ok(Some(input.purple().to_string())) + $$; + "#; + Spi::run(definition)?; + + let retval = Spi::get_one_with_args::( + r#" + SELECT colorize($1); + "#, + vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], + ); + assert!(retval.is_ok()); + assert!(retval.unwrap().is_some()); + + // Regression test: A previous version of PL/Rust would abort if this was called twice, so call it twice: + let retval = Spi::get_one_with_args::( + r#" + SELECT colorize($1); + "#, + vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], + ); + assert!(retval.is_ok()); + assert!(retval.unwrap().is_some()); + Ok(()) + } +} diff --git a/plrust/src/tests.rs b/plrust/src/tests.rs index 06f10c66..6ff17da4 100644 --- a/plrust/src/tests.rs +++ b/plrust/src/tests.rs @@ -10,1394 +10,8 @@ Use of this source code is governed by the PostgreSQL license that can be found #[cfg(any(test, feature = "pg_test"))] #[pgrx::pg_schema] mod tests { - use pgrx::{datum::IntoDatum, prelude::*}; - use std::error::Error; - - // Bootstrap a testing table for non-immutable functions - extension_sql!( - r#" - CREATE TABLE contributors_pets ( - id serial8 not null primary key, - name text - ); - INSERT INTO contributors_pets (name) VALUES ('Brandy'); - INSERT INTO contributors_pets (name) VALUES ('Nami'); - INSERT INTO contributors_pets (name) VALUES ('Sally'); - INSERT INTO contributors_pets (name) VALUES ('Anchovy'); - "#, - name = "create_contributors_pets", - ); - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_basic() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION sum_array(a BIGINT[]) RETURNS BIGINT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - Ok(Some(a.into_iter().map(|v| v.unwrap_or_default()).sum())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one_with_args::( - r#" - SELECT sum_array($1); - "#, - vec![( - PgBuiltInOids::INT4ARRAYOID.oid(), - vec![1, 2, 3].into_datum(), - )], - ); - assert_eq!(retval, Ok(Some(6))); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_text_array_with_initial_null() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION collect_text_array_with_initial_null(i text[]) RETURNS text[] - STRICT - LANGUAGE plrust AS - $$ - Ok(Some(i.into_iter().map(|s| s.map(|s| s.to_string())).collect())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one::>>( - r#" - SELECT collect_text_array_with_initial_null(ARRAY[NULL, 'a', 'b', 'c']); - "#, - ); - assert_eq!( - retval, - Ok(Some(vec![ - None, - Some("a".into()), - Some("b".into()), - Some("c".into()) - ])) - ); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_update() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION update_me() RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - Ok(String::from("booper").into()) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one( - r#" - SELECT update_me(); - "#, - ); - assert_eq!(retval, Ok(Some("booper"))); - - let definition = r#" - CREATE OR REPLACE FUNCTION update_me() RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - Ok(Some(String::from("swooper"))) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one( - r#" - SELECT update_me(); - "#, - ); - assert_eq!(retval, Ok(Some("swooper"))); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_spi() -> spi::Result<()> { - let random_definition = r#" - CREATE FUNCTION random_contributor_pet() RETURNS TEXT - STRICT - LANGUAGE PLRUST AS - $$ - Ok(Spi::get_one("SELECT name FROM contributors_pets ORDER BY random() LIMIT 1")?) - $$; - "#; - Spi::run(random_definition)?; - - let retval = Spi::get_one::( - r#" - SELECT random_contributor_pet(); - "#, - ); - assert!(retval.is_ok()); - assert!(retval.unwrap().is_some()); - - let specific_definition = r#" - CREATE FUNCTION contributor_pet(name TEXT) RETURNS BIGINT - STRICT - LANGUAGE PLRUST AS - $$ - use pgrx::IntoDatum; - Ok(Spi::get_one_with_args( - "SELECT id FROM contributors_pets WHERE name = $1", - vec![(PgBuiltInOids::TEXTOID.oid(), name.into_datum())], - )?) - $$; - "#; - Spi::run(specific_definition)?; - - let retval = Spi::get_one::( - r#" - SELECT contributor_pet('Nami'); - "#, - ); - assert_eq!(retval, Ok(Some(2))); - Ok(()) - } - - #[pg_test] - #[cfg(not(feature = "sandboxed"))] - #[search_path(@extschema@)] - fn plrust_deps_supported() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION colorize(input TEXT) RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [dependencies] - owo-colors = "3" - [code] - use owo_colors::OwoColorize; - Ok(Some(input.purple().to_string())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one_with_args::( - r#" - SELECT colorize($1); - "#, - vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], - ); - assert!(retval.is_ok()); - assert!(retval.unwrap().is_some()); - - // Regression test: A previous version of PL/Rust would abort if this was called twice, so call it twice: - let retval = Spi::get_one_with_args::( - r#" - SELECT colorize($1); - "#, - vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], - ); - assert!(retval.is_ok()); - assert!(retval.unwrap().is_some()); - Ok(()) - } - - #[pg_test] - #[cfg(not(feature = "sandboxed"))] - #[search_path(@extschema@)] - fn plrust_deps_supported_semver_parse() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION colorize(input TEXT) RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [dependencies] - owo-colors = ">2" - [code] - use owo_colors::OwoColorize; - Ok(Some(input.purple().to_string())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one_with_args::( - r#" - SELECT colorize($1); - "#, - vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], - ); - assert!(retval.is_ok()); - assert!(retval.unwrap().is_some()); - - // Regression test: A previous version of PL/Rust would abort if this was called twice, so call it twice: - let retval = Spi::get_one_with_args::( - r#" - SELECT colorize($1); - "#, - vec![(PgBuiltInOids::TEXTOID.oid(), "Nami".into_datum())], - ); - assert!(retval.is_ok()); - assert!(retval.unwrap().is_some()); - Ok(()) - } - - #[pg_test] - #[cfg(not(feature = "sandboxed"))] - #[search_path(@extschema@)] - fn plrust_deps_supported_deps_in_toml_table() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION say_hello() RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [dependencies] - tokio = ">=1" - owo-colors = "3" - [code] - Ok(Some("hello".to_string())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one_with_args::( - r#" - SELECT say_hello(); - "#, - vec![(PgBuiltInOids::TEXTOID.oid(), "hello".into_datum())], - ); - assert_eq!(retval, Ok(Some("hello".to_string()))); - Ok(()) - } - - #[pg_test] - #[cfg(not(feature = "sandboxed"))] - #[search_path(@extschema@)] - fn plrust_deps_not_supported() { - let definition = r#" - CREATE FUNCTION colorize(input TEXT) RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [dependencies] - regex = "1.6.5" - [code] - Ok(Some("test")) - $$; - "#; - let res = std::panic::catch_unwind(|| { - Spi::run(definition).expect("SQL for plrust_deps_not_supported() failed") - }); - assert!(res.is_err()); - } - - // Regression for #348 - #[pg_test] - #[cfg(not(feature = "sandboxed"))] - #[search_path(@extschema@)] - fn plrust_rand_dep() { - let definition = r#" - CREATE FUNCTION rust_rand() RETURNS INT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [dependencies] - rand = "0.8.5" - [code] - Ok(Some(rand::random())) - $$; - "#; - Spi::run(definition).unwrap(); - - let rand = Spi::get_one::("SELECT rust_rand()").unwrap(); - assert!(rand.is_some()); - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_returns_setof() -> spi::Result<()> { - let definition = r#" - CREATE OR REPLACE FUNCTION boop_srf(names TEXT[]) RETURNS SETOF TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - Ok(Some(::pgrx::iter::SetOfIterator::new(names.into_iter().map(|maybe| maybe.map(|name| name.to_string() + " was booped!"))))) - $$; - "#; - Spi::run(definition)?; - - let retval: spi::Result<_> = Spi::connect(|client| { - let mut table = client.select( - "SELECT * FROM boop_srf(ARRAY['Nami', 'Brandy'])", - None, - None, - )?; - - let mut found = vec![]; - while table.next().is_some() { - let value = table.get_one::()?; - found.push(value) - } - - Ok(Some(found)) - }); - - assert_eq!( - retval, - Ok(Some(vec![ - Some("Nami was booped!".into()), - Some("Brandy was booped!".into()), - ])) - ); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_aggregate() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION plrust_sum_state(state INT, next INT) RETURNS INT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - Ok(Some(state + next)) - $$; - CREATE AGGREGATE plrust_sum(INT) - ( - SFUNC = plrust_sum_state, - STYPE = INT, - INITCOND = '0' - ); - "#; - Spi::run(definition)?; - - let retval = Spi::get_one::( - r#" - SELECT plrust_sum(value) FROM UNNEST(ARRAY [1, 2, 3]) as value; - "#, - ); - assert_eq!(retval, Ok(Some(6))); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_trigger() -> spi::Result<()> { - let definition = r#" - CREATE TABLE dogs ( - name TEXT, - scritches INT NOT NULL DEFAULT 0 - ); - - CREATE FUNCTION pet_trigger() RETURNS trigger AS $$ - let mut new = trigger.new().unwrap().into_owned(); - - let field = "scritches"; - - match new.get_by_name::(field)? { - Some(val) => new.set_by_name(field, val + 1)?, - None => (), - } - - Ok(Some(new)) - $$ LANGUAGE plrust; - - CREATE TRIGGER pet_trigger BEFORE INSERT OR UPDATE ON dogs - FOR EACH ROW EXECUTE FUNCTION pet_trigger(); - - INSERT INTO dogs (name) VALUES ('Nami'); - "#; - Spi::run(definition)?; - - let retval = Spi::get_one::( - r#" - SELECT scritches FROM dogs; - "#, - ); - assert_eq!(retval, Ok(Some(1))); - Ok(()) - } - - #[cfg(feature = "trusted")] - #[pg_test] - #[search_path(@extschema@)] - fn postgrestd_dont_make_files() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION make_file(filename TEXT) RETURNS TEXT - LANGUAGE PLRUST AS - $$ - Ok(std::fs::File::create(filename.unwrap_or("/somewhere/files/dont/belong.txt")) - .err() - .map(|e| e.to_string())) - $$; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one_with_args::( - r#" - SELECT make_file($1); - "#, - vec![( - PgBuiltInOids::TEXTOID.oid(), - "/an/evil/place/to/put/a/file.txt".into_datum(), - )], - ); - assert_eq!( - retval, - Ok(Some("operation not supported on this platform".to_string())) - ); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "yup"] - fn pgrx_can_panic() { - panic!("yup") - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "yup"] - fn plrust_can_panic() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION shut_up_and_explode() - RETURNS text AS - $$ - panic!("yup"); - Ok(None) - $$ LANGUAGE plrust; - "#; - - Spi::run(definition)?; - let retval = Spi::get_one::("SELECT shut_up_and_explode();\n"); - assert_eq!(retval, Ok(None)); - Ok(()) - } - - #[cfg(feature = "trusted")] - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "Failed to execute command"] - fn postgrestd_subprocesses_panic() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION say_hello() - RETURNS text AS - $$ - let out = std::process::Command::new("echo") - .arg("Hello world") - .stdout(std::process::Stdio::piped()) - .output() - .expect("Failed to execute command"); - Ok(Some(String::from_utf8_lossy(&out.stdout).to_string())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one::("SELECT say_hello();\n"); - assert_eq!(retval, Ok(Some("Hello world\n".into()))); - Ok(()) - } - - #[cfg(feature = "trusted")] - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "error: the `include_str`, `include_bytes`, and `include` macros are forbidden"] - fn postgrestd_no_include_str() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION include_str() - RETURNS text AS - $$ - let s = include_str!("/etc/passwd"); - Ok(Some(s.into())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - - let retval = Spi::get_one::("SELECT include_str();\n")?; - assert_eq!(retval.unwrap(), ""); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[cfg(feature = "trusted")] - #[should_panic = "No such file or directory (os error 2)"] - fn plrustc_include_exists_no_access() { - // This file is created in CI and exists, but can only be accessed by - // root. Check that the actual access is reported as file not found (we - // should be ensuring that via - // `PLRUSTC_USER_CRATE_ALLOWED_SOURCE_PATHS`). We don't need to gate - // this test on CI, since the file is unlikely to exist outside of CI - // (so the test will pass). - let definition = r#" - CREATE FUNCTION include_no_access() - RETURNS text AS $$ - include!("/var/ci-stuff/secret_rust_files/const_foo.rs"); - Ok(Some(format!("{BAR}"))) - $$ LANGUAGE plrust; - "#; - Spi::run(definition).unwrap() - } - - #[pg_test] - #[search_path(@extschema@)] - #[cfg(feature = "trusted")] - #[should_panic = "No such file or directory (os error 2)"] - fn plrustc_include_exists_external() { - // This file is created in CI, exists, and can be accessed by anybody, - // but the actual access is forbidden via - // `PLRUSTC_USER_CRATE_ALLOWED_SOURCE_PATHS`. We don't need to gate this test on - // CI, since the file is unlikely to exist outside of CI, so the test - // will pass anyway. - let definition = r#" - CREATE FUNCTION include_exists_external() - RETURNS text AS $$ - include!("/var/ci-stuff/const_bar.rs"); - Ok(Some(format!("{BAR}"))) - $$ LANGUAGE plrust; - "#; - Spi::run(definition).unwrap(); - } - - #[pg_test] - #[search_path(@extschema@)] - #[cfg(feature = "trusted")] - #[should_panic = "No such file or directory (os error 2)"] - fn plrustc_include_made_up() { - // This file does not exist, and should be reported as such. - let definition = r#" - CREATE FUNCTION include_madeup() - RETURNS int AS $$ - include!("/made/up/path/lol.rs"); - Ok(Some(1)) - $$ LANGUAGE plrust; - "#; - Spi::run(definition).unwrap(); - } - - #[pg_test] - #[search_path(@extschema@)] - #[cfg(feature = "trusted")] - #[should_panic = "No such file or directory (os error 2)"] - fn plrustc_include_path_traversal() { - use std::path::PathBuf; - let workdir = crate::gucs::work_dir(); - let wd: PathBuf = workdir - .canonicalize() - .ok() - .expect("Failed to canonicalize workdir"); - // Produce a path that looks like - // `/allowed/path/here/../../../illegal/path/here` and check that it's - // rejected, in order to ensure we are not succeptable to path traversal - // attacks. - let mut evil_path = wd.clone(); - for _ in wd.ancestors().skip(1) { - evil_path.push(".."); - } - debug_assert_eq!( - evil_path - .canonicalize() - .ok() - .expect("Failed to produce unpath") - .to_str(), - Some("/") - ); - evil_path.push("var/ci-stuff/const_bar.rs"); - // This file does not exist, and should be reported as such. - let definition = format!( - r#"CREATE FUNCTION include_sneaky_traversal() - RETURNS int AS $$ - include!({evil_path:?}); - Ok(Some(1)) - $$ LANGUAGE plrust;"# - ); - Spi::run(&definition).unwrap(); - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "error: usage of an `unsafe` block"] - fn plrust_block_unsafe_annotated() -> spi::Result<()> { - // PL/Rust should block creating obvious, correctly-annotated usage of unsafe code - let definition = r#" - CREATE FUNCTION naughty() - RETURNS text AS - $$ - use std::{os::raw as ffi, str, ffi::CStr}; - let int:u32 = 0xDEADBEEF; - // Note that it is always safe to create a pointer. - let ptr = int as *mut u64; - // What is unsafe is dereferencing it - let cstr = unsafe { - ptr.write(0x00_1BADC0DE_00); - CStr::from_ptr(ptr.cast::()) - }; - Ok(str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "call to unsafe function is unsafe and requires unsafe block"] - fn plrust_block_unsafe_hidden() -> spi::Result<()> { - // PL/Rust should not allow hidden injection of unsafe code - // that may rely on the way PGRX expands into `unsafe fn` to "sneak in" - let definition = r#" - CREATE FUNCTION naughty() - RETURNS text AS - $$ - use std::{os::raw as ffi, str, ffi::CStr}; - let int:u32 = 0xDEADBEEF; - let ptr = int as *mut u64; - ptr.write(0x00_1BADC0DE_00); - let cstr = CStr::from_ptr(ptr.cast::()); - Ok(str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "error: the `env` and `option_env` macros are forbidden"] - #[cfg(feature = "trusted")] - fn plrust_block_env() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION get_path() RETURNS text AS $$ - let path = env!("PATH"); - Ok(Some(path.to_string())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "error: the `env` and `option_env` macros are forbidden"] - #[cfg(feature = "trusted")] - fn plrust_block_option_env() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION try_get_path() RETURNS text AS $$ - match option_env!("PATH") { - None => Ok(None), - Some(s) => Ok(Some(s.to_string())) - } - $$ LANGUAGE plrust; - "#; - Spi::run(definition) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "error: usage of an `unsafe` block"] - fn plrust_block_unsafe_plutonium() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION super_safe() - RETURNS text AS - $$ - [dependencies] - plutonium = "*" - - [code] - use std::{os::raw as ffi, str, ffi::CStr}; - use plutonium::safe; - - #[safe] - fn super_safe() -> Option { - let int: u32 = 0xDEADBEEF; - let ptr = int as *mut u64; - ptr.write(0x00_1BADC0DE_00); - let cstr = CStr::from_ptr(ptr.cast::()); - str::from_utf8(cstr.to_bytes()).ok().map(|s| s.to_owned()) - } - - Ok(super_safe()) - $$ LANGUAGE plrust; - "#; - Spi::run(definition) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "xxx"] - #[ignore] - fn plrust_pgloglevel_dont_allcaps_panic() -> spi::Result<()> { - // This test attempts to annihilate the database. - // It relies on the existing assumption that tests are run in the same Postgres instance, - // so this test will make all tests "flaky" if Postgres suddenly goes down with it. - let definition = r#" - CREATE FUNCTION dont_allcaps_panic() - RETURNS text AS - $$ - ereport!(PANIC, PgSqlErrorCode::ERRCODE_INTERNAL_ERROR, "If other tests completed, PL/Rust did not actually destroy the entire database, \ - But if you see this in the error output, something might be wrong."); - Ok(Some("lol".into())) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let retval = Spi::get_one::("SELECT dont_allcaps_panic();\n"); - assert_eq!(retval, Ok(Some("lol".into()))); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_call_1st() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION ret_1st(a int, b int) - RETURNS int AS - $$ - Ok(a) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result_1 = Spi::get_one::("SELECT ret_1st(1, 2);\n"); - assert_eq!(Ok(Some(1)), result_1); // may get: Some(1) - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_call_2nd() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION ret_2nd(a int, b int) - RETURNS int AS - $$ - Ok(b) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result_2 = Spi::get_one::("SELECT ret_2nd(1, 2);\n"); - assert_eq!(Ok(Some(2)), result_2); // may get: Some(2) - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_call_me() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION pick_ret(a int, b int, pick int) - RETURNS int AS - $$ - Ok(match pick { - Some(0) => a, - Some(1) => b, - _ => None, - }) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result_a = Spi::get_one::("SELECT pick_ret(3, 4, 0);"); - let result_b = Spi::get_one::("SELECT pick_ret(5, 6, 1);"); - let result_c = Spi::get_one::("SELECT pick_ret(7, 8, 2);"); - let result_z = Spi::get_one::("SELECT pick_ret(9, 99, -1);"); - assert_eq!(Ok(Some(3)), result_a); // may get: Some(4) or None - assert_eq!(Ok(Some(6)), result_b); // may get: None - assert_eq!(Ok(None), result_c); - assert_eq!(Ok(None), result_z); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - fn plrust_call_me_call_me() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION ret_1st(a int, b int) - RETURNS int AS - $$ - Ok(a) - $$ LANGUAGE plrust; - - CREATE FUNCTION ret_2nd(a int, b int) - RETURNS int AS - $$ - Ok(b) - $$ LANGUAGE plrust; - - CREATE FUNCTION pick_ret(a int, b int, pick int) - RETURNS int AS - $$ - Ok(match pick { - Some(0) => a, - Some(1) => b, - _ => None, - }) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result_1 = Spi::get_one::("SELECT ret_1st(1, 2);\n"); - let result_2 = Spi::get_one::("SELECT ret_2nd(1, 2);\n"); - let result_a = Spi::get_one::("SELECT pick_ret(3, 4, 0);"); - let result_b = Spi::get_one::("SELECT pick_ret(5, 6, 1);"); - let result_c = Spi::get_one::("SELECT pick_ret(7, 8, 2);"); - let result_z = Spi::get_one::("SELECT pick_ret(9, 99, -1);"); - assert_eq!(Ok(None), result_z); - assert_eq!(Ok(None), result_c); - assert_eq!(Ok(Some(6)), result_b); // may get: None - assert_eq!(Ok(Some(3)), result_a); // may get: Some(4) or None - assert_eq!(Ok(Some(2)), result_2); // may get: Some(1) - assert_eq!(Ok(Some(1)), result_1); // may get: Some(2) - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "parameter name \"a\" used more than once"] - fn plrust_dup_args() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION not_unique(a int, a int) - RETURNS int AS - $$ - Ok(a) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result = Spi::get_one::("SELECT not_unique(1, 2);\n"); - assert_eq!(Ok(Some(1)), result); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "PL/Rust does not support unnamed arguments"] - fn plrust_defaulting_dup_args() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION not_unique(int, arg0 int) - RETURNS int AS - $$ - Ok(arg0) - $$ LANGUAGE plrust; - "#; - Spi::run(definition)?; - let result = Spi::get_one::("SELECT not_unique(1, 2);\n"); - assert_eq!(Ok(Some(1)), result); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "plrust functions cannot have their STRICT property altered"] - fn plrust_cant_change_strict_off() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION cant_change_strict_off() - RETURNS int - LANGUAGE plrust - AS $$ Ok(Some(1)) $$; - "#; - Spi::run(definition)?; - Spi::run("ALTER FUNCTION cant_change_strict_off() CALLED ON NULL INPUT") - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic = "plrust functions cannot have their STRICT property altered"] - fn plrust_cant_change_strict_on() -> spi::Result<()> { - let definition = r#" - CREATE FUNCTION cant_change_strict_on() - RETURNS int - LANGUAGE plrust - AS $$ Ok(Some(1)) $$; - "#; - Spi::run(definition)?; - Spi::run("ALTER FUNCTION cant_change_strict_on() RETURNS NULL ON NULL INPUT") - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic(expected = "error: declaration of a function with `export_name`")] - fn plrust_block_unsafe_export_name() -> spi::Result<()> { - // A separate test covers #[no_mangle], but what about #[export_name]? - // Same idea. This tries to collide with free, which may symbol clash, - // or might override depending on how the OS and loader feel today. - // Let's not leave it up to forces beyond our control. - let definition = r#" - CREATE OR REPLACE FUNCTION export_hacked_free() RETURNS BIGINT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - #[export_name = "free"] - pub extern "C" fn own_free(ptr: *mut c_void) { - // the contents don't matter - } - - Ok(Some(1)) - $$; - "#; - Spi::run(definition)?; - let result = Spi::get_one::("SELECT export_hacked_free();\n"); - assert_eq!(Ok(Some(1)), result); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic(expected = "error: declaration of a static with `link_section`")] - fn plrust_block_unsafe_link_section() -> spi::Result<()> { - let definition = r#" - CREATE OR REPLACE FUNCTION link_evil_section() RETURNS BIGINT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - #[link_section = ".init_array"] - pub static INITIALIZE: &[u8; 136] = &GOGO; - - #[link_section = ".text"] - pub static GOGO: [u8; 136] = [ - 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, - 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, - 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, - 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, - 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, - 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, - 106, 59, 88, 15, 5, - ]; - - Ok(Some(1)) - $$; - "#; - Spi::run(definition)?; - let result = Spi::get_one::("SELECT link_evil_section();\n"); - assert_eq!(Ok(Some(1)), result); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic(expected = "error: declaration of a `no_mangle` static")] - fn plrust_block_unsafe_no_mangle() -> spi::Result<()> { - let definition = r#" - CREATE OR REPLACE FUNCTION not_mangled() RETURNS BIGINT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - #[no_mangle] - #[link_section = ".init_array"] - pub static INITIALIZE: &[u8; 136] = &GOGO; - - #[no_mangle] - #[link_section = ".text"] - pub static GOGO: [u8; 136] = [ - 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, 72, 184, 46, 99, 104, 111, 46, 114, 105, 1, 72, 49, 4, - 36, 72, 137, 231, 106, 1, 254, 12, 36, 72, 184, 99, 102, 105, 108, 101, 49, 50, 51, 80, 72, - 184, 114, 47, 116, 109, 112, 47, 112, 111, 80, 72, 184, 111, 117, 99, 104, 32, 47, 118, 97, - 80, 72, 184, 115, 114, 47, 98, 105, 110, 47, 116, 80, 72, 184, 1, 1, 1, 1, 1, 1, 1, 1, 80, - 72, 184, 114, 105, 1, 44, 98, 1, 46, 116, 72, 49, 4, 36, 49, 246, 86, 106, 14, 94, 72, 1, - 230, 86, 106, 19, 94, 72, 1, 230, 86, 106, 24, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, - 106, 59, 88, 15, 5, - ]; - - Ok(Some(1)) - $$; - "#; - Spi::run(definition)?; - let result = Spi::get_one::("SELECT not_mangled();\n"); - assert_eq!(Ok(Some(1)), result); - Ok(()) - } - - #[pg_test] - #[should_panic(expected = "issue78 works")] - fn test_issue_78() -> spi::Result<()> { - let sql = r#"CREATE OR REPLACE FUNCTION raise_error() RETURNS TEXT - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - pgrx::error!("issue78 works"); - Ok(Some("hi".to_string())) - $$;"#; - Spi::run(sql)?; - Spi::get_one::("SELECT raise_error()")?; - Ok(()) - } - - #[pg_test] - fn test_issue_79() -> spi::Result<()> { - let sql = r#" - create or replace function fn1(i int) returns int strict language plrust as $$ - [code] - notice!("{}", "fn1 started"); - let cmd = format!("select fn2({})", i); - Spi::connect(|client| - { - client.select(&cmd, None, None); - }); - notice!("{}", "fn1 finished"); - Ok(Some(1)) - $$; - - create or replace function fn2(i int) returns int strict language plrust as $$ - [code] - notice!("{}", "fn2 started"); - notice!("{}", "fn2 finished"); - Ok(Some(2)) - $$; - "#; - Spi::run(sql)?; - assert_eq!(Ok(Some(1)), Spi::get_one::("SELECT fn1(1)")); - Ok(()) - } - - #[pg_test] - fn replace_function() -> spi::Result<()> { - Spi::run("CREATE FUNCTION replace_me() RETURNS int LANGUAGE plrust AS $$ Ok(Some(1)) $$")?; - assert_eq!(Ok(Some(1)), Spi::get_one("SELECT replace_me()")); - - Spi::run( - "CREATE OR REPLACE FUNCTION replace_me() RETURNS int LANGUAGE plrust AS $$ Ok(Some(2)) $$", - )?; - assert_eq!(Ok(Some(2)), Spi::get_one("SELECT replace_me()")); - Ok(()) - } - - #[pg_test] - fn test_point() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION test_point(p point) RETURNS point LANGUAGE plrust AS $$ Ok(p) $$"#, - )?; - let p = Spi::get_one::("SELECT test_point('42, 99'::point);")? - .expect("SPI result was null"); - assert_eq!(p.x, 42.0); - assert_eq!(p.y, 99.0); - Ok(()) - } - - #[pg_test] - fn test_box() -> spi::Result<()> { - Spi::run(r#"CREATE FUNCTION test_box(b box) RETURNS box LANGUAGE plrust AS $$ Ok(b) $$"#)?; - let b = Spi::get_one::("SELECT test_box('1,2,3,4'::box);")? - .expect("SPI result was null"); - assert_eq!(b.high.x, 3.0); - assert_eq!(b.high.y, 4.0); - assert_eq!(b.low.x, 1.0); - assert_eq!(b.low.y, 2.0); - Ok(()) - } - - #[pg_test] - fn test_uuid() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION test_uuid(u uuid) RETURNS uuid LANGUAGE plrust AS $$ Ok(u) $$"#, - )?; - let u = Spi::get_one::( - "SELECT test_uuid('e4176a4d-790c-4750-85b7-665d72471173'::uuid);", - )? - .expect("SPI result was null"); - assert_eq!( - u, - pgrx::Uuid::from_bytes([ - 0xe4, 0x17, 0x6a, 0x4d, 0x79, 0x0c, 0x47, 0x50, 0x85, 0xb7, 0x66, 0x5d, 0x72, 0x47, - 0x11, 0x73 - ]) - ); - - Ok(()) - } - - #[pg_test] - fn test_int4range() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION test_int4range(r int4range) RETURNS int4range LANGUAGE plrust AS $$ Ok(r) $$"#, - )?; - let r = Spi::get_one::>("SELECT test_int4range('[1, 10)'::int4range);")? - .expect("SPI result was null"); - assert_eq!(r, (1..10).into()); - Ok(()) - } - - #[pg_test] - fn test_int8range() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION test_int8range(r int8range) RETURNS int8range LANGUAGE plrust AS $$ Ok(r) $$"#, - )?; - let r = Spi::get_one::>("SELECT test_int8range('[1, 10)'::int8range);")? - .expect("SPI result was null"); - assert_eq!(r, (1..10).into()); - Ok(()) - } - - #[pg_test] - fn test_numrange() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION test_numrange(r numrange) RETURNS numrange LANGUAGE plrust AS $$ Ok(r) $$"#, - )?; - let r = Spi::get_one::>("SELECT test_numrange('[1, 10]'::numrange);")? - .expect("SPI result was null"); - assert_eq!( - r, - Range::new( - AnyNumeric::try_from(1.0f32).unwrap(), - AnyNumeric::try_from(10.0f32).unwrap() - ) - ); - Ok(()) - } - - #[pg_test] - fn test_tid_roundtrip() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION tid_roundtrip(t tid) RETURNS tid LANGUAGE plrust AS $$ Ok(t) $$"#, - )?; - let tid = Spi::get_one::("SELECT tid_roundtrip('(42, 99)'::tid)")? - .expect("SPI result was null"); - let (blockno, offno) = pgrx::item_pointer_get_both(tid); - assert_eq!(blockno, 42); - assert_eq!(offno, 99); - Ok(()) - } - - #[pg_test] - fn test_return_bytea() -> spi::Result<()> { - Spi::run( - r#"CREATE FUNCTION return_bytea() RETURNS bytea LANGUAGE plrust AS $$ Ok(Some(vec![1,2,3])) $$"#, - )?; - let bytes = Spi::get_one::>("SELECT return_bytea()")?.expect("SPI result was null"); - assert_eq!(bytes, vec![1, 2, 3]); - Ok(()) - } - - #[pg_test] - fn test_cstring_roundtrip() -> Result<(), Box> { - use std::ffi::CStr; - Spi::run( - r#"CREATE FUNCTION cstring_roundtrip(s cstring) RETURNS cstring STRICT LANGUAGE plrust as $$ Ok(Some(s.into())) $$;"#, - )?; - let cstr = Spi::get_one::<&CStr>("SELECT cstring_roundtrip('hello')")? - .expect("SPI result was null"); - let expected = CStr::from_bytes_with_nul(b"hello\0")?; - assert_eq!(cstr, expected); - Ok(()) - } - - #[pg_test] - fn test_daterange() -> Result<(), Box> { - Spi::run( - r#"CREATE FUNCTION test_daterange(r daterange) RETURNS daterange LANGUAGE plrust AS $$ Ok(r) $$"#, - )?; - let r = Spi::get_one::>( - "SELECT test_daterange('[1977-03-20, 1980-01-01)'::daterange);", - )? - .expect("SPI result was null"); - assert_eq!( - r, - Range::new( - Date::new(1977, 3, 20)?, - RangeBound::Exclusive(Date::new(1980, 01, 01)?) - ) - ); - Ok(()) - } - - #[pg_test] - fn test_tsrange() -> Result<(), Box> { - Spi::run( - r#"CREATE FUNCTION test_tsrange(p tsrange) RETURNS tsrange LANGUAGE plrust AS $$ Ok(p) $$"#, - )?; - let r = Spi::get_one::>( - "SELECT test_tsrange('[1977-03-20, 1980-01-01)'::tsrange);", - )? - .expect("SPI result was null"); - assert_eq!( - r, - Range::new( - Timestamp::new(1977, 3, 20, 0, 0, 0.0)?, - RangeBound::Exclusive(Timestamp::new(1980, 01, 01, 0, 0, 0.0)?) - ) - ); - Ok(()) - } - - #[pg_test] - fn test_tstzrange() -> Result<(), Box> { - Spi::run( - r#"CREATE FUNCTION test_tstzrange(p tstzrange) RETURNS tstzrange LANGUAGE plrust AS $$ Ok(p) $$"#, - )?; - let r = Spi::get_one::>( - "SELECT test_tstzrange('[1977-03-20, 1980-01-01)'::tstzrange);", - )? - .expect("SPI result was null"); - assert_eq!( - r, - Range::new( - TimestampWithTimeZone::new(1977, 3, 20, 0, 0, 0.0)?, - RangeBound::Exclusive(TimestampWithTimeZone::new(1980, 01, 01, 0, 0, 0.0)?) - ) - ); - Ok(()) - } - - #[pg_test] - fn test_interval() -> Result<(), Box> { - Spi::run( - r#"CREATE FUNCTION get_interval_hours(i interval) RETURNS numeric STRICT LANGUAGE plrust AS $$ Ok(i.extract_part(DateTimeParts::Hour)) $$"#, - )?; - let hours = - Spi::get_one::("SELECT get_interval_hours('3 days 9 hours 12 seconds')")? - .expect("SPI result was null"); - assert_eq!(hours, AnyNumeric::from(9)); - Ok(()) - } - - #[cfg(feature = "trusted")] - #[pg_test] - #[search_path(@extschema@)] - fn postgrestd_net_is_unsupported() -> spi::Result<()> { - let sql = r#" - create or replace function pt106() returns text - IMMUTABLE STRICT - LANGUAGE PLRUST AS - $$ - [code] - use std::net::TcpStream; - - Ok(TcpStream::connect("127.0.0.1:22").err().map(|e| e.to_string())) - $$"#; - Spi::run(sql)?; - let string = Spi::get_one::("SELECT pt106()")?.expect("Unconditional return"); - assert_eq!("operation not supported on this platform", &string); - Ok(()) - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic(expected = "PL/Rust does not support unnamed arguments")] - fn unnamed_args() -> spi::Result<()> { - Spi::run("CREATE FUNCTION unnamed_arg(int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic(expected = "PL/Rust does not support unnamed arguments")] - fn named_unnamed_args() -> spi::Result<()> { - Spi::run("CREATE FUNCTION named_unnamed_arg(bob text, int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") - } - - #[pg_test] - #[search_path(@extschema@)] - #[should_panic( - expected = "is an invalid Rust identifier and cannot be used as an argument name" - )] - fn invalid_arg_identifier() -> spi::Result<()> { - Spi::run("CREATE FUNCTION invalid_arg_identifier(\"this isn't a valid rust identifier\" int) RETURNS int LANGUAGE plrust as $$ Ok(None) $$;") - } - - #[pg_test] - fn test_srf_one_col() -> spi::Result<()> { - Spi::run( - "CREATE FUNCTION srf_one_col() RETURNS TABLE (a int) LANGUAGE plrust AS $$ - Ok(Some(TableIterator::new(vec![( Some(1), )].into_iter()))) - $$;", - )?; - - let a = Spi::get_one::("SELECT * FROM srf_one_col()")?; - assert_eq!(a, Some(1)); - - Ok(()) - } - - #[pg_test] - fn test_srf_two_col() -> spi::Result<()> { - Spi::run( - "CREATE FUNCTION srf_two_col() RETURNS TABLE (a int, b int) LANGUAGE plrust AS $$ - Ok(Some(TableIterator::new(vec![(Some(1), Some(2))].into_iter()))) - $$;", - )?; - - let (a, b) = Spi::get_two::("SELECT * FROM srf_two_col()")?; - assert_eq!(a, Some(1)); - assert_eq!(b, Some(2)); - - Ok(()) - } - - #[pg_test] - fn test_udt() -> spi::Result<()> { - Spi::run( - r#" -CREATE TYPE person AS ( - name text, - age float8 -); - -create function make_person(name text, age float8) returns person - strict parallel safe - language plrust as -$$ - // create the Heap Tuple representation of the SQL type `person` - let mut p = PgHeapTuple::new_composite_type("person")?; - - // set a few of its attributes - // - // Runtime errors can occur if the attribute name is invalid or if the Rust type of the value - // is not compatible with the backing SQL type for that attribute. Hence the use of the `?` operator - p.set_by_name("name", name)?; - p.set_by_name("age", age)?; - - // return the `person` - Ok(Some(p)) -$$; - -create function get_person_name(p person) returns text - strict parallel safe - language plrust as -$$ - // `p` is a `PgHeapTuple` over the underlying data for `person` - Ok(p.get_by_name("name")?) -$$; - -create function get_person_age(p person) returns float8 - strict parallel safe - language plrust as -$$ - // `p` is a `PgHeapTuple` over the underlying data for `person` - Ok(p.get_by_name("age")?) -$$; - -create function get_person_attribute(p person, attname text) returns text - strict parallel safe - language plrust as -$$ - match attname.to_lowercase().as_str() { - "age" => { - let age:Option = p.get_by_name("age")?; - Ok(age.map(|v| v.to_string())) - }, - "name" => { - Ok(p.get_by_name("name")?) - }, - _ => panic!("unknown attribute: `{attname}`") - } -$$; - -create operator ->> (function = get_person_attribute, leftarg = person, rightarg = text); - -create table people -( - id serial8 not null primary key, - p person -); - -insert into people (p) values (make_person('Johnny', 46.24)); -insert into people (p) values (make_person('Joe', 99.09)); -insert into people (p) values (make_person('Dr. Beverly Crusher of the Starship Enterprise', 32.0)); - "#, - )?; - - let johnny = Spi::get_one::>( - "SELECT p FROM people WHERE p->>'name' = 'Johnny';", - )? - .expect("SPI result was null"); - - let age = johnny.get_by_name::("age")?.expect("age was null"); - assert_eq!(age, 46.24); - - Ok(()) - } + use pgrx::prelude::*; + use pgrx::spi; #[pg_test] fn test_allowed_dependencies() -> spi::Result<()> {