From f44c4bd9b0380c13228bd492059b0c4b762a5825 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Mon, 10 Aug 2026 21:41:49 -0400 Subject: [PATCH 1/6] Fix one typo in a recent comment Typo was introduced in 234afe4. --- .../pljava/example/annotation/MishandledExceptions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java index 95073d3f9..50506b2e1 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 - Tada AB and other contributors, as listed below. + * Copyright (c) 2025-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License From c289209173138f0da08459d82a8d534e77fadca0 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2026 09:24:23 -0400 Subject: [PATCH 2/6] Prepare to split CI/integration jshell script Using jshell for interactive testing as well as for automated CI will be simpler if the jshell script is first split into integration (for automated testing), jpsql (for interactive work, albeit with a far more rudimentary UX than psql offers!), and common, factored out and referred to by both. In this preparatory commit, simply rename integration as common, to improve git's ability to track the changes in the commit to follow. --- CI/{integration => common} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename CI/{integration => common} (99%) diff --git a/CI/integration b/CI/common similarity index 99% rename from CI/integration rename to CI/common index fd8da7676..48f5017c4 100644 --- a/CI/integration +++ b/CI/common @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2025 Tada AB and other contributors, as listed below. + * Copyright (c) 2020-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License From 0095fc1c92acb9021b27b8b4a276b8225d29ff14 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2026 09:39:10 -0400 Subject: [PATCH 3/6] Split up the CI/integration jshell script Retain in 'common' the setup steps and method declarations needed by both 'integration' and 'jpsql'. Rework 'integration' as a script that opens 'common', runs the usual tests, and exits with appropriate status, and supply 'jpsql' as a script that opens 'common', spins up a test node, installs PL/Java and the examples, and leaves a jshell prompt with the node running and a JDBC connection open. --- CI/common | 422 ++++++------------------------------------------- CI/integration | 377 +++++++++++++++++++++++++++++++++++++++++++ CI/jpsql | 60 +++++++ 3 files changed, 482 insertions(+), 377 deletions(-) create mode 100644 CI/integration create mode 100644 CI/jpsql diff --git a/CI/common b/CI/common index 48f5017c4..08df17edc 100644 --- a/CI/common +++ b/CI/common @@ -10,7 +10,10 @@ * Chapman Flack * Kartik Ohri * - * This jshell script performs basic integration tests for PL/Java's CI. + * This jshell script is a common preamble loaded by the 'integration' script + * (used for automated integration runs) and the 'jpsql' script (used for + * interactive work in jshell with a freshly-spun-up cluster, though of course + * jshell presents a much more rudimentary UI than psql proper). * * It must be executed with the built PL/Java packaged jar (produced by the * pljava-packaging subproject) on the classpath, as well as a PGJDBC or @@ -60,17 +63,6 @@ * here can be useful, if this script is run with the needed permissions to * write in the PostgreSQL install locations, when combined with redirectError * if running under PowerShell, which would otherwise mess up the output. - * - * The script does not (yet) produce output in any standardized format such as - * TAP. The output will include numerous , , , or - * elements. If it runs to completion there will be a line with counts - * for info, warning, error, and ng. The count of ng results includes errors - * and certain warnings. The tests that are run from the deployment descriptor - * of the pljava-examples jar report test failures as warnings (to avoid cutting - * short the test as an error would), so those warnings are counted in ng. - * - * jshell will exit with a nonzero status if ng > 0 or anything else was seen - * to go wrong or the script did not run to completion. */ boolean succeeding = false; // begin pessimistic @@ -89,6 +81,7 @@ import static java.nio.file.Paths.get; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; +import java.sql.Statement; import org.postgresql.pljava.packaging.Node; import static org.postgresql.pljava.packaging.Node.q; import static org.postgresql.pljava.packaging.Node.stateMachine; @@ -220,20 +213,14 @@ throws Exception ); } -try ( - AutoCloseable t1 = n1.initialized_cluster(tweaks); - AutoCloseable t2 = n1.started_server(serverOptions, tweaks); -) +/* + * Load the PL/Java extension and return true for success. + */ +boolean loadPLJava(Node n) throws Exception { - int pgMajorVersion; - - try ( Connection c = n1.connect() ) + try ( Connection c = n.connect() ) { - pgMajorVersion = c.getMetaData().getDatabaseMajorVersion(); - - succeeding = true; // become optimistic, will be using &= below - - succeeding &= stateMachine( + return stateMachine( "create extension no result", null, @@ -250,365 +237,46 @@ try ( (o,p,q) -> null == o ); } +} - /* - * Get a new connection; 'create extension' always sets a near-silent - * logging level, and PL/Java only checks once at VM start time, so in - * the same session where 'create extension' was done, logging is - * somewhat suppressed. - */ - try ( Connection c = n1.connect() ) - { - succeeding &= stateMachine( - "saxon path examples path", - null, - - Node.installSaxonAndExamplesAndPath(c, - System.getProperty("mavenRepo"), - System.getProperty("saxonVer"), - true) - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - // states 1,2: diagnostics* then a void result set (saxon install) - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - - // states 3,4: diagnostics* then a void result set (set classpath) - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 3 : -4, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 5 : false, - - // states 5,6: diagnostics* then void result set (example install) - (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 5 : -6, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 7 : false, - - // states 7,8: diagnostics* then a void result set (set classpath) - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 7 : -8, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 9 : false, - - // state 9: must be end of input - (o,p,q) -> null == o - ); - - /* - * Exercise TrialPolicy some. Need another connection to change - * vmoptions. Uses some example functions, so insert here before the - * test of undeploying the examples. - */ - try ( Connection c2 = n1.connect() ) - { - succeeding &= useTrialPolicy(n1, c2, List.of( - "grant {", - " permission", - " org.postgresql.pljava.policy.TrialPolicy$Permission;", - "};" - )); - - PreparedStatement tryForbiddenRead = c2.prepareStatement( - "SELECT" + - " CASE WHEN javatest.java_getsystemproperty('java.home')" + - " OPERATOR(pg_catalog.=) ?" + - " THEN javatest.logmessage('INFO', 'trial policy test ok')" + - " ELSE javatest.logmessage('WARNING', 'trial policy test ng')" + - " END" - ); - - tryForbiddenRead.setString(1, javaHome); - - succeeding &= stateMachine( - "try to read a forbidden property", - null, - - q(tryForbiddenRead, tryForbiddenRead::execute) - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> null == o - ); - // done with connection c2 - } - - /* - * Spin up an http server with a little jar file to serve, and test - * that install_jar works with an http: url. - * - * First make a little jar empty but for a deployment descriptor. - */ - String ddrName = "foo.ddr"; - Attributes a = new Attributes(); - a.putValue("SQLJDeploymentDescriptor", "TRUE"); - Manifest m = new Manifest(); - m.getEntries().put(ddrName, a); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - JarOutputStream jos = new JarOutputStream(baos, m); - jos.putNextEntry(new ZipEntry(ddrName)); - jos.write( - ( - "SQLActions[]={\n\"BEGIN INSTALL\n" + - "SELECT javatest.logmessage('INFO'," + - " 'jar installed from http');\n" + - "END INSTALL\",\n\"BEGIN REMOVE\n" + - "BEGIN dummy\n" + - "END dummy;\n" + - "END REMOVE\"\n}\n" - ).getBytes(UTF_8) - ); - jos.closeEntry(); - jos.close(); - byte[] jar = baos.toByteArray(); - - /* - * Now an http server. - */ - HttpServer hs = - HttpServer.create(new InetSocketAddress("localhost", 0), 0); - - try ( - Connection c2 = n1.connect(); - AutoCloseable t = ((Supplier)() -> - { - hs.start(); - return () -> hs.stop(0); - } - ).get() - ) - { - InetSocketAddress addr = hs.getAddress(); - - String id = "bar", pw = "baz"; - - URL u = new URI( - "http", id+':'+pw, addr.getHostString(), addr.getPort(), - "/foo.jar", null, null - ).toURL(); - - HttpContext hc = hs.createContext( - u.getPath(), - new HttpHandler() - { - @Override - public void handle(HttpExchange t) throws IOException - { - try ( InputStream is = t.getRequestBody() ) { - is.readAllBytes(); - } - t.getResponseHeaders().add( - "Content-Type", "application/java-archive"); - t.sendResponseHeaders(200, jar.length); - try ( OutputStream os = t.getResponseBody() ) { - os.write(jar); - } - } - } - ); - - hc.setAuthenticator( - new BasicAuthenticator("CI realm") - // ("CI realm", UTF_8) only available in Java 14 or later - { - @Override - public boolean checkCredentials(String c_id, String c_pw) - { - return id.equals(c_id) && pw.equals(c_pw); - } - } - ); - - succeeding &= useTrialPolicy(n1, c2, List.of( - "grant codebase \"${org.postgresql.pljava.codesource}\" {", - " permission", - " java.net.URLPermission \"http:*\", \"GET:Accept\";", - "};" - )); - - succeeding &= stateMachine( - "install a jar over http", - null, - - Node.installJar(c2, u.toString(), "foo", true) - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> null == o - ); - - // done with connection c2 again, and the http server - } - - /* - * Also confirm that the generated undeploy actions work. - */ - succeeding &= stateMachine( - "remove jar void result", - null, - - q(c, "SELECT sqlj.remove_jar('examples', true)") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> null == o - ); - - /* - * Get another new connection and make sure the extension can be - * loaded in a non-superuser session. - */ - try ( Connection c2 = n1.connect() ) - { - succeeding &= stateMachine( - "become non-superuser", - null, - - q(c2, - "CREATE ROLE alice;" + - "GRANT USAGE ON SCHEMA sqlj TO alice;" + - "SET SESSION AUTHORIZATION alice") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - NOTHING_OR_PGJDBC_ZERO_COUNT, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); - - succeeding &= stateMachine( - "load as non-superuser", - null, - - q(c2, "SELECT null::pg_catalog.void" + - " FROM sqlj.get_classpath('public')") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> null == o - ); - // done with connection c2 again - } - - /* - * Make sure the extension drops cleanly and nothing - * is left in sqlj. - */ - succeeding &= stateMachine( - "drop extension and schema no result", - null, - - q(c, "DROP EXTENSION pljava;DROP SCHEMA sqlj") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); - } - - /* - * Get another new connection and confirm that the old, pre-extension, - * LOAD method of installing PL/Java works. It is largely obsolete in - * the era of extensions, but still covers the use case of installing - * PL/Java without admin access on the server filesystem to where - * CREATE EXTENSION requires the files to be; they can still be - * installed in some other writable location the server can read, and - * pljava.module_path set to the right locations of the jars, and the - * correct shared-object path given to LOAD. - * - * Also test the after-the-fact packaging up with CREATE EXTENSION - * FROM unpackaged. That officially goes away in PG 13, where the - * equivalent sequence - * CREATE EXTENSION pljava VERSION unpackaged - * \c - * ALTER EXTENSION pljava UPDATE - * should be tested instead. - */ - try ( Connection c = n1.connect() ) - { - succeeding &= stateMachine( - "load as non-extension", - null, - - Node.loadPLJava(c) - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); - - if ( 13 <= pgMajorVersion ) - { - succeeding &= stateMachine( - "create unpackaged (PG >= 13)", - null, - - q(c, "CREATE EXTENSION pljava VERSION unpackaged") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); - } - } +/* + * Load the PL/Java (and Saxon) examples and execute the deployment descriptors, + * which include regression tests. + * Return true if that was successfully done, which doesn't mean the tests all + * necessarily passed, unless results.get("ng") is also zero afterward. + */ +boolean loadExamplesAndTest(Connection c) throws Exception +{ + return stateMachine( + "saxon path examples path", + null, - /* - * CREATE EXTENSION FROM unpackaged (or the second half of the - * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE - * sequence) has to happen over a new connection. - */ - try ( Connection c = n1.connect() ) - { - succeeding &= stateMachine( - "package after loading", - null, + Node.installSaxonAndExamplesAndPath(c, + System.getProperty("mavenRepo"), + System.getProperty("saxonVer"), + true) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), - q(c, 13 > pgMajorVersion - ? "CREATE EXTENSION pljava FROM unpackaged" - : "ALTER EXTENSION pljava UPDATE") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), + // states 1,2: diagnostics* then a void result set (saxon install) + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); + // states 3,4: diagnostics* then a void result set (set classpath) + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 3 : -4, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 5 : false, - /* - * Again make sure extension drops cleanly with nothing left behind. - */ - succeeding &= stateMachine( - "drop extension and schema no result", - null, + // states 5,6: diagnostics* then void result set (example install) + (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 5 : -6, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 7 : false, - q(c, "DROP EXTENSION pljava;DROP SCHEMA sqlj") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), + // states 7,8: diagnostics* then a void result set (set classpath) + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 7 : -8, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 9 : false, - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - NOTHING_OR_PGJDBC_ZERO_COUNT, - NOTHING_OR_PGJDBC_ZERO_COUNT, - (o,p,q) -> null == o - ); - } -} catch ( Throwable t ) -{ - succeeding = false; - throw t; + // state 9: must be end of input + (o,p,q) -> null == o + ); } -System.out.println(results); -succeeding &= (0 == results.get("ng")); -System.exit(succeeding ? 0 : 1); +int pgMajorVersion; diff --git a/CI/integration b/CI/integration new file mode 100644 index 000000000..7690cd8e2 --- /dev/null +++ b/CI/integration @@ -0,0 +1,377 @@ +/* + * Copyright (c) 2020-2026 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + * Kartik Ohri + * + * This jshell script performs basic integration tests for PL/Java's CI. + * + * The current directory when executing this script should be the parent + * of the 'CI' directory containing this file. This file begins by opening + * the 'common' file (also in this directory), using the path 'CI/common'. + * See the comments in that file for details on everything that needs to be + * on jshell's command line to execute this script. + * + * The script does not (yet) produce output in any standardized format such as + * TAP. The output will include numerous , , , or + * elements. If it runs to completion there will be a line with counts + * for info, warning, error, and ng. The count of ng results includes errors + * and certain warnings. The tests that are run from the deployment descriptor + * of the pljava-examples jar report test failures as warnings (to avoid cutting + * short the test as an error would), so those warnings are counted in ng. + * + * jshell will exit with a nonzero status if ng > 0 or anything else was seen + * to go wrong or the script did not run to completion. + */ +/open CI/common + +try ( + AutoCloseable t1 = n1.initialized_cluster(tweaks); + AutoCloseable t2 = n1.started_server(serverOptions, tweaks); +) +{ + try ( Connection c = n1.connect() ) + { + pgMajorVersion = c.getMetaData().getDatabaseMajorVersion(); + + succeeding = true; // become optimistic, will be using &= below + + succeeding &= loadPLJava(n1); + + /* + * Most regression testing happens here, driven by the deployment + * descriptors executed as the examples are loaded. The function returns + * true as long as it succeeded in doing that, which does not necessarily + * mean the tests all passed. That must be checked later in this script + * by verifying that results.get("ng") is zero. + */ + succeeding &= loadExamplesAndTest(c); + + /* + * Exercise TrialPolicy some. Need another connection to change + * vmoptions. Uses some example functions, so insert here before the + * test of undeploying the examples. + */ + try ( Connection c2 = n1.connect() ) + { + succeeding &= useTrialPolicy(n1, c2, List.of( + "grant {", + " permission", + " org.postgresql.pljava.policy.TrialPolicy$Permission;", + "};" + )); + + PreparedStatement tryForbiddenRead = c2.prepareStatement( + "SELECT" + + " CASE WHEN javatest.java_getsystemproperty('java.home')" + + " OPERATOR(pg_catalog.=) ?" + + " THEN javatest.logmessage('INFO', 'trial policy test ok')" + + " ELSE javatest.logmessage('WARNING', 'trial policy test ng')" + + " END" + ); + + tryForbiddenRead.setString(1, javaHome); + + succeeding &= stateMachine( + "try to read a forbidden property", + null, + + q(tryForbiddenRead, tryForbiddenRead::execute) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + // done with connection c2 + } + + /* + * Spin up an http server with a little jar file to serve, and test + * that install_jar works with an http: url. + * + * First make a little jar empty but for a deployment descriptor. + */ + String ddrName = "foo.ddr"; + Attributes a = new Attributes(); + a.putValue("SQLJDeploymentDescriptor", "TRUE"); + Manifest m = new Manifest(); + m.getEntries().put(ddrName, a); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + JarOutputStream jos = new JarOutputStream(baos, m); + jos.putNextEntry(new ZipEntry(ddrName)); + jos.write( + ( + "SQLActions[]={\n\"BEGIN INSTALL\n" + + "SELECT javatest.logmessage('INFO'," + + " 'jar installed from http');\n" + + "END INSTALL\",\n\"BEGIN REMOVE\n" + + "BEGIN dummy\n" + + "END dummy;\n" + + "END REMOVE\"\n}\n" + ).getBytes(UTF_8) + ); + jos.closeEntry(); + jos.close(); + byte[] jar = baos.toByteArray(); + + /* + * Now an http server. + */ + HttpServer hs = + HttpServer.create(new InetSocketAddress("localhost", 0), 0); + + try ( + Connection c2 = n1.connect(); + AutoCloseable t = ((Supplier)() -> + { + hs.start(); + return () -> hs.stop(0); + } + ).get() + ) + { + InetSocketAddress addr = hs.getAddress(); + + String id = "bar", pw = "baz"; + + URL u = new URI( + "http", id+':'+pw, addr.getHostString(), addr.getPort(), + "/foo.jar", null, null + ).toURL(); + + HttpContext hc = hs.createContext( + u.getPath(), + new HttpHandler() + { + @Override + public void handle(HttpExchange t) throws IOException + { + try ( InputStream is = t.getRequestBody() ) { + is.readAllBytes(); + } + t.getResponseHeaders().add( + "Content-Type", "application/java-archive"); + t.sendResponseHeaders(200, jar.length); + try ( OutputStream os = t.getResponseBody() ) { + os.write(jar); + } + } + } + ); + + hc.setAuthenticator( + new BasicAuthenticator("CI realm") + // ("CI realm", UTF_8) only available in Java 14 or later + { + @Override + public boolean checkCredentials(String c_id, String c_pw) + { + return id.equals(c_id) && pw.equals(c_pw); + } + } + ); + + succeeding &= useTrialPolicy(n1, c2, List.of( + "grant codebase \"${org.postgresql.pljava.codesource}\" {", + " permission", + " java.net.URLPermission \"http:*\", \"GET:Accept\";", + "};" + )); + + succeeding &= stateMachine( + "install a jar over http", + null, + + Node.installJar(c2, u.toString(), "foo", true) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error", "warning")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + + // done with connection c2 again, and the http server + } + + /* + * Also confirm that the generated undeploy actions work. + */ + succeeding &= stateMachine( + "remove jar void result", + null, + + q(c, "SELECT sqlj.remove_jar('examples', true)") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + + /* + * Get another new connection and make sure the extension can be + * loaded in a non-superuser session. + */ + try ( Connection c2 = n1.connect() ) + { + succeeding &= stateMachine( + "become non-superuser", + null, + + q(c2, + "CREATE ROLE alice;" + + "GRANT USAGE ON SCHEMA sqlj TO alice;" + + "SET SESSION AUTHORIZATION alice") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + NOTHING_OR_PGJDBC_ZERO_COUNT, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + + succeeding &= stateMachine( + "load as non-superuser", + null, + + q(c2, "SELECT null::pg_catalog.void" + + " FROM sqlj.get_classpath('public')") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + (o,p,q) -> isVoidResultSet(o, 1, 1) ? 3 : false, + (o,p,q) -> null == o + ); + // done with connection c2 again + } + + /* + * Make sure the extension drops cleanly and nothing + * is left in sqlj. + */ + succeeding &= stateMachine( + "drop extension and schema no result", + null, + + q(c, "DROP EXTENSION pljava;DROP SCHEMA sqlj") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + } + + /* + * Get another new connection and confirm that the old, pre-extension, + * LOAD method of installing PL/Java works. It is largely obsolete in + * the era of extensions, but still covers the use case of installing + * PL/Java without admin access on the server filesystem to where + * CREATE EXTENSION requires the files to be; they can still be + * installed in some other writable location the server can read, and + * pljava.module_path set to the right locations of the jars, and the + * correct shared-object path given to LOAD. + * + * Also test the after-the-fact packaging up with CREATE EXTENSION + * FROM unpackaged. That officially goes away in PG 13, where the + * equivalent sequence + * CREATE EXTENSION pljava VERSION unpackaged + * \c + * ALTER EXTENSION pljava UPDATE + * should be tested instead. + */ + try ( Connection c = n1.connect() ) + { + succeeding &= stateMachine( + "load as non-extension", + null, + + Node.loadPLJava(c) + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + + if ( 13 <= pgMajorVersion ) + { + succeeding &= stateMachine( + "create unpackaged (PG >= 13)", + null, + + q(c, "CREATE EXTENSION pljava VERSION unpackaged") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + } + } + + /* + * CREATE EXTENSION FROM unpackaged (or the second half of the + * PG >= 13 CREATE EXTENSION VERSION unpackaged;ALTER EXTENSION UPDATE + * sequence) has to happen over a new connection. + */ + try ( Connection c = n1.connect() ) + { + succeeding &= stateMachine( + "package after loading", + null, + + q(c, 13 > pgMajorVersion + ? "CREATE EXTENSION pljava FROM unpackaged" + : "ALTER EXTENSION pljava UPDATE") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + + /* + * Again make sure extension drops cleanly with nothing left behind. + */ + succeeding &= stateMachine( + "drop extension and schema no result", + null, + + q(c, "DROP EXTENSION pljava;DROP SCHEMA sqlj") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + NOTHING_OR_PGJDBC_ZERO_COUNT, + NOTHING_OR_PGJDBC_ZERO_COUNT, + (o,p,q) -> null == o + ); + } +} catch ( Throwable t ) +{ + succeeding = false; + throw t; +} + +System.out.println(results); +succeeding &= (0 == results.get("ng")); +System.exit(succeeding ? 0 : 1); diff --git a/CI/jpsql b/CI/jpsql new file mode 100644 index 000000000..75f72c0c4 --- /dev/null +++ b/CI/jpsql @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2020-2026 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + * Kartik Ohri + * + * This jshell script spins up a new PostgreSQL Node (available as the variable + * n1) in a temporary area, starts it, and establishes one JDBC Connection + * available as the variable c. The PL/Java extension and examples will be + * loaded, and then the qp method can be used for interacting with the database + * over the established connection, as a sort of (very rudimentary!) psql + * alternative. + * + * The current directory when executing this script should be the parent + * of the 'CI' directory containing this file. This file begins by opening + * the 'common' file (also in this directory), using the path 'CI/common'. + * See the comments in that file for details on everything that needs to be + * on jshell's command line to execute this script. + * + * jshell will exit with a nonzero status if ng > 0 or anything else was seen + * to go wrong or the script did not run to completion. + */ +/open CI/common + +Map jdwp(boolean suspend) +{ + String guc = "pljava.vmoptions=" + vmopts + String.format( + " -agentlib:jdwp=transport=dt_socket,server=y,suspend=%s,address=localhost:0", + suspend ? 'y' : 'n'); + String opt = "-c " + guc.replaceAll("([\\s\\\\])", "\\\\$1"); + return Map.of("options", opt); +} + +import static org.postgresql.pljava.packaging.Node.qp; + +n1.init(tweaks); +n1.start(serverOptions, tweaks); + +Connection c = n1.connect(); + +pgMajorVersion = c.getMetaData().getDatabaseMajorVersion(); + +succeeding = loadPLJava(n1); + +/* + * Most regression testing happens here, driven by the deployment + * descriptors executed as the examples are loaded. The function returns + * true as long as it succeeded in doing that, which does not necessarily + * mean the tests all passed. That can be checked later, if needed, + * by verifying that results.get("ng") is zero. + */ +succeeding &= loadExamplesAndTest(c); + +/vars succeeding results From c290c7cd782b005a355e2e7be4b8f94efe5292c1 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2026 10:51:33 -0400 Subject: [PATCH 4/6] Use newer MacOS runners for GitHub Actions CI Try the Intel flavor of MacOS 15 and the ARM flavor of MacOS 26, and install PostgreSQL 18 rather than 17. --- .github/workflows/ci-lazypg.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-lazypg.yml b/.github/workflows/ci-lazypg.yml index fbff5f858..57b649107 100644 --- a/.github/workflows/ci-lazypg.yml +++ b/.github/workflows/ci-lazypg.yml @@ -26,12 +26,12 @@ jobs: oscc: - os: ubuntu-latest cc: gcc - - os: macos-13 + - os: macos-15-intel cc: clang - pg: 17 - - os: macos-14 + pg: 18 + - os: macos-26 cc: clang - pg: 17 + pg: 18 - os: windows-latest cc: msvc - os: windows-latest From 601f43ed8d576c14c24ae97ddb4ab443925a2cd5 Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2026 10:53:37 -0400 Subject: [PATCH 5/6] Include Java 25 for CI testing, vice 23 25 is an LTS release; 23 wasn't. --- .github/workflows/ci-lazypg.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-lazypg.yml b/.github/workflows/ci-lazypg.yml index 57b649107..1be8f8a0a 100644 --- a/.github/workflows/ci-lazypg.yml +++ b/.github/workflows/ci-lazypg.yml @@ -36,12 +36,12 @@ jobs: cc: msvc - os: windows-latest cc: mingw - java: [11, 17, 21, 23] + java: [11, 17, 21, 25] exclude: - oscc: {os: windows-latest} java: 17 - oscc: {os: windows-latest} - java: 23 + java: 25 steps: From 88de0aa42b5c3d85177ff1283869e5d3a104740c Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Tue, 11 Aug 2026 11:31:30 -0400 Subject: [PATCH 6/6] Address two CI warnings from newer C compilers One variable with missing 'static' was recently introduced by me (234afe4); the other has been that way since 4e75a06 in 2005! --- pljava-so/src/main/c/Exception.c | 5 +++-- pljava-so/src/main/c/JNICalls.c | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index 86e9f2ae2..2c7c862e8 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,7 +27,8 @@ jmethodID Class_getCanonicalName; jclass ServerException_class; jmethodID ServerException_getErrorData; -jmethodID ServerException_obtain; + +static jmethodID ServerException_obtain; jclass Throwable_class; jmethodID Throwable_getMessage; diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index 4e496b0da..230fb3736 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2026 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -22,7 +22,7 @@ #include "pljava/type/ErrorData.h" #include "pljava/type/String.h" -JNIEnv* jniEnv; +static JNIEnv* jniEnv; jint (JNICALL *pljava_createvm)(JavaVM **, void **, void *); void* mainThreadId; /* declared in pljava.h */