diff --git a/.github/workflows/ci-lazypg.yml b/.github/workflows/ci-lazypg.yml new file mode 100644 index 000000000..fbff5f858 --- /dev/null +++ b/.github/workflows/ci-lazypg.yml @@ -0,0 +1,288 @@ +# This workflow will build and test PL/Java against a version of PostgreSQL +# lazily obtained (either preinstalled in the GitHub Actions runner environment, +# or obtained from a package repository if the runner does not provide one). +# Arrange for the matrix to include a pg version, for cases where one must be +# installed. + +name: CI lazy getting PostgreSQL + +permissions: + contents: read + +on: + push: + branches: [ master, REL1_6_STABLE ] + pull_request: + branches: [ master, REL1_6_STABLE ] + +jobs: + build: + if: true + + runs-on: ${{ matrix.oscc.os }} + continue-on-error: true + strategy: + matrix: + oscc: + - os: ubuntu-latest + cc: gcc + - os: macos-13 + cc: clang + pg: 17 + - os: macos-14 + cc: clang + pg: 17 + - os: windows-latest + cc: msvc + - os: windows-latest + cc: mingw + java: [11, 17, 21, 23] + exclude: + - oscc: {os: windows-latest} + java: 17 + - oscc: {os: windows-latest} + java: 23 + + steps: + + - name: Check for JDK preinstalled + id: jdkcheck + shell: bash + env: + JAVAVER: ${{ matrix.java }} + run: | + if + candidate="JAVA_HOME_${JAVAVER}_${RUNNER_ARCH}" + echo -n "Environment contains $candidate? " + [[ -n ${!candidate+set} ]] + then + echo yes + echo >>"$GITHUB_ENV" "JAVA_HOME=${!candidate}" + echo >>"$GITHUB_OUTPUT" java_found=true + elif + candidate="JAVA_HOME_${JAVAVER}_$(tr A-Z a-z <<<${RUNNER_ARCH})" + echo -ne 'no\n'"Environment contains $candidate? " + [[ -n ${!candidate+set} ]] + then + echo yes + echo >>"$GITHUB_ENV" "JAVA_HOME=${!candidate}" + echo >>"$GITHUB_OUTPUT" java_found=true + else + echo -e 'no\n'"only: ${!JAVA_HOME_*}" + echo >>"$GITHUB_OUTPUT" java_found=false + fi + + - name: Fetch a JDK + if: ${{ 'false' == steps.jdkcheck.outputs.java_found }} + uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b + with: + distribution: temurin + java-version: ${{ matrix.java }} + + - name: Compute absolute paths for java and jshell + shell: bash + run: | + if [[ $RUNNER_OS == Windows ]] + then + echo >>"$GITHUB_ENV" "ABS_JAVA=$JAVA_HOME"'\bin\java' + echo >>"$GITHUB_ENV" "ABS_JSHELL=$JAVA_HOME"'\bin\jshell' + else + echo >>"$GITHUB_ENV" "ABS_JAVA=$JAVA_HOME/bin/java" + echo >>"$GITHUB_ENV" "ABS_JSHELL=$JAVA_HOME/bin/jshell" + fi + + - name: Set PGCONFIG in environment, getting PostgreSQL if needed (!mingw) + if: ${{ 'mingw' != matrix.oscc.cc }} + shell: bash + env: + PGVER: ${{ matrix.oscc.pg }} + run: | + if [[ $RUNNER_OS == Linux ]] + then + echo >>"$GITHUB_ENV" PGCONFIG=pg_config + elif [[ $RUNNER_OS == Windows ]] + then + echo >>"$GITHUB_ENV" PGCONFIG="$PGBIN"'\pg_config' + elif [[ $RUNNER_OS == macOS ]] + then + echo '::group::brew update' + brew update + echo '::endgroup::' + echo "::group::brew install postgresql@$PGVER" + # HOMEBREW_GITHUB_ACTIONS will suppress the formula's initdb + HOMEBREW_GITHUB_ACTIONS=1 HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK=1 \ + brew install postgresql@"$PGVER" + echo '::endgroup::' + pfx=$(brew --prefix postgresql@"$PGVER") + echo >>"$GITHUB_ENV" PGCONFIG="$pfx/bin/pg_config" + fi + + - name: Set PGCONFIG in environment, getting gcc and PostgreSQL (mingw) + if: ${{ 'mingw' == matrix.oscc.cc }} + shell: C:\shells\msys2bash.cmd {0} + run: | + pacman -S --noconfirm \ + mingw-w64-x86_64-gcc \ + mingw-w64-x86_64-postgresql + echo >>"$GITHUB_ENV" PGCONFIG='c:\msys64\mingw64\bin\pg_config' + + - name: Report Java, Maven, and PostgreSQL versions + shell: bash + run: | + "$ABS_JAVA" -version + mvn --version + "$PGCONFIG" + + - name: Obtain PG development files (Ubuntu, PGDG) + if: ${{ 'Linux' == runner.os }} + run: | + pgver=$("$PGCONFIG" --version) + pgver=${pgver##PostgreSQL } + pgver=${pgver%% *} + pgver=${pgver%.*} + echo '::group::Install PGDG key and repo' + curl -s -S https://www.postgresql.org/media/keys/ACCC4CF8.asc | + gpg --dearmor | + sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg + echo \ + deb \ + http://apt.postgresql.org/pub/repos/apt \ + "$(lsb_release -cs)-pgdg" \ + main | + sudo tee /etc/apt/sources.list.d/pgdg.list + echo '::endgroup::' + echo '::group::apt-get update' + sudo apt-get update + echo '::endgroup::' + echo "::group::apt-get install postgresql-server-dev-$pgver" + sudo apt-get install postgresql-server-dev-"$pgver" libkrb5-dev + echo '::endgroup::' + + - name: Confirm PostgreSQL development files are present + shell: python + run: | + from os import getenv + from os.path import join + from re import sub + from subprocess import check_output + + pgconfig = getenv('PGCONFIG') + + def ask_pg_config(what): + return check_output([pgconfig, '--'+what]).splitlines()[0] + + pgch = join(ask_pg_config('includedir-server'), b'pg_config.h') + + with open(pgch, 'r') as f: + line = [ln for ln in f if ln.startswith('#define PG_VERSION_STR ')][0] + + vers = sub(r'#define PG_VERSION_STR "(.*)"\n', r'\1', line) + + print('PostgreSQL development files are present:', vers, sep='\n') + + - name: Check out PL/Java + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + path: pljava + + - name: Set plethora of MSVC environment variables (Windows MSVC) + if: ${{ 'Windows' == runner.os && 'msvc' == matrix.oscc.cc }} + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 + + - name: Build PL/Java (Windows MSVC) + if: ${{ 'Windows' == runner.os && 'msvc' == matrix.oscc.cc }} + working-directory: pljava + # shell: cmd because of the issue described for ilammy/msvc-dev-cmd + # with Actions bash prepending stuff to the just-carefully-created PATH + shell: cmd + run: | + mvn clean install --batch-mode ^ + -Dpgsql.pgconfig="%PGCONFIG%" ^ + -Psaxon-examples -Ppgjdbc ^ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + + - name: Build PL/Java (Linux, macOS) + if: ${{ 'Windows' != runner.os }} + working-directory: pljava + run: | + mvn clean install --batch-mode \ + -Dpgsql.pgconfig="$PGCONFIG" \ + -Psaxon-examples -Ppgjdbc \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + + - name: Build PL/Java (Windows MinGW-w64) + if: ${{ 'Windows' == runner.os && 'mingw' == matrix.oscc.cc }} + working-directory: pljava + shell: C:\shells\msys2bash.cmd {0} + run: | + PATH='/c/msys64/mingw64/bin:'"$PATH" + "$M2"/mvn clean install --batch-mode \ + -Dpgsql.pgconfig="$PGCONFIG" \ + -Psaxon-examples -Ppgjdbc \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn + + - name: Install and test PL/Java + working-directory: pljava + shell: bash + run: | + packageJar=$(find pljava-packaging -name pljava-pg*.jar -print) + + mavenRepo="$HOME/.m2/repository" + + saxonVer=$( + find "$mavenRepo/net/sf/saxon/Saxon-HE" \ + -name 'Saxon-HE-*.jar' -print | + sort | + tail -n 1 + ) + saxonVer=${saxonVer%/*} + saxonVer=${saxonVer##*/} + + jdbcJar=$( + find "$mavenRepo/org/postgresql/postgresql" \ + -name 'postgresql-*.jar' -print | + sort | + tail -n 1 + ) + + # + # The runner on a Unix-like OS is running as a non-privileged user, but + # has passwordless sudo available (needed to install the PL/Java files + # into the system directories where the supplied PostgreSQL lives). By + # contrast, on Windows the runner has admin privilege, and can install + # the files without any fuss (but later below, pg_ctl will have to be + # used when starting PostgreSQL; pg_ctl has a Windows-specific ability + # to drop admin privs so postgres will not refuse to start). + # + # The Git for Windows bash environment includes a find command, and the + # things found have unixy paths returned. Make them Windowsy here, with + # a hardcoded assumption they start with /c which should become c: (as + # appears to be the case in the Windows runner currently). + # + echo '::group::Install files from the package jar' + if [[ $RUNNER_OS == Windows ]] + then + pathSep=';' + "$ABS_JAVA" -Dpgconfig="$PGCONFIG" -jar "$packageJar" + function toWindowsPath() { + local p + p="c:${1#/c}" + printf "%s" "${p//\//\\}" + } + jdbcJar="$(toWindowsPath "$jdbcJar")" + mavenRepo="$(toWindowsPath "$mavenRepo")" + else + pathSep=':' + sudo "$ABS_JAVA" -Dpgconfig="$PGCONFIG" -jar "$packageJar" + fi + echo '::endgroup::' + + "$ABS_JSHELL" \ + -execution local \ + "-J--class-path=$packageJar$pathSep$jdbcJar" \ + "--class-path=$packageJar" \ + "-J--add-modules=java.sql.rowset,jdk.httpserver" \ + "-J-Dpgconfig=$PGCONFIG" \ + "-J-DmavenRepo=$mavenRepo" \ + "-J-DsaxonVer=$saxonVer" \ + CI/integration diff --git a/.github/workflows/ci-runnerpg.yml b/.github/workflows/ci-runnerpg.yml deleted file mode 100644 index 9eea358cd..000000000 --- a/.github/workflows/ci-runnerpg.yml +++ /dev/null @@ -1,719 +0,0 @@ -# This workflow will build and test PL/Java against the version of PostgreSQL -# preinstalled in the GitHub Actions runner environment. Naturally, this one -# does not have a PostgreSQL version in the build matrix. The version that's -# preinstalled is the version you get. - -name: CI with runner-supplied PostgreSQL version - -permissions: - contents: read - -on: - push: - branches: [ master, REL1_6_STABLE ] - pull_request: - branches: [ master, REL1_6_STABLE ] - -jobs: - build: - if: true - - runs-on: ${{ matrix.oscc.os }} - continue-on-error: true - strategy: - matrix: - oscc: - - os: ubuntu-latest - cc: gcc - - os: macos-12 - cc: clang -# - os: windows-latest -# cc: msvc -# - os: windows-latest -# cc: mingw - java: [9, 11, 17, 19, 21] - - steps: - - - name: Check out PL/Java - uses: actions/checkout@v2 - with: - path: pljava - - - name: Set up JDK - uses: actions/setup-java@v1 - with: - java-version: ${{ matrix.java }} - - - name: Report Java, Maven, and PostgreSQL versions (Linux, macOS) - if: ${{ 'Windows' != runner.os }} - run: | - java -version - mvn --version - pg_config - - - name: Report Java, Maven, and PostgreSQL versions (Windows) - if: ${{ 'Windows' == runner.os }} - run: | - java -version - mvn --version - & "$Env:PGBIN\pg_config" - - - name: Obtain PG development files (Ubuntu, PGDG) - if: ${{ 'Linux' == runner.os }} - run: | - curl -s -S https://www.postgresql.org/media/keys/ACCC4CF8.asc | - gpg --dearmor | - sudo dd of=/etc/apt/trusted.gpg.d/apt.postgresql.org.gpg - echo \ - deb \ - http://apt.postgresql.org/pub/repos/apt \ - "$(lsb_release -cs)-pgdg" \ - main | - sudo tee /etc/apt/sources.list.d/pgdg.list - sudo apt-get update - sudo apt-get install postgresql-server-dev-14 libkrb5-dev - - - name: Build PL/Java (Linux, macOS) - if: ${{ 'Windows' != runner.os }} - working-directory: pljava - run: | - mvn clean install --batch-mode \ - -Psaxon-examples -Ppgjdbc-ng \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn - - - name: Build PL/Java (Windows MinGW-w64) - if: ${{ 'Windows' == runner.os && 'mingw' == matrix.oscc.cc }} - working-directory: pljava - # - # GitHub Actions will allow 'bash' as a shell choice, even on a Windows - # runner, in which case it's the bash from Git for Windows. That isn't the - # same as the msys64\usr\bin\bash that we want; what's more, while both - # rely on a cygwin DLL, they don't rely on the same one, and an attempt - # to exec one from the other leads to a "fatal error - cygheap base - # mismatch". So, the bash we want has to be started by something other - # than the bash we've got. In this case, set shell: to a command that - # will use cmd to start the right bash. - # - # Some of the MinGW magic is set up by the bash profile run at "login", so - # bash must be started with -l. That profile ends with a cd $HOME, so to - # avoid changing the current directory, set HOME=. first (credit for that: - # https://superuser.com/a/806371). As set above, . is really the pljava - # working-directory, so the bash script should start by resetting HOME to - # the path of its parent. - # - # The runner is provisioned with a very long PATH that includes separate - # bin directories for pre-provisioned packages. The MinGW profile replaces - # that with a much shorter path, so mvn and pg_config below must be given - # as absolute paths (using M2 and PGBIN supplied in the environment) or - # they won't be found. As long as mvn itself can be found, it is able - # to find java without difficulty, using the JAVA_HOME that is also in - # the environment. - # - # Those existing variables in the environment are all spelled in Windows - # style with drive letters, colons, and backslashes, rather than the MinGW - # unixy style, but the mingw bash doesn't seem to object. - # - # If you use the runner-supplied bash to examine the environment, you will - # see MSYSTEM=MINGW64 already in it, but that apparently is something the - # runner-supplied bash does. It must be set here before invoking the MinGW - # bash directly. - # - env: - HOME: . - MSYSTEM: MINGW64 - shell: 'cmd /C "c:\msys64\usr\bin\bash -l "{0}""' - run: | - HOME=$( (cd .. && pwd) ) - "$M2"/mvn clean install --batch-mode \ - -Dpgsql.pgconfig="$PGBIN"'\pg_config' \ - -Psaxon-examples -Ppgjdbc-ng \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn - - - name: Install and test PL/Java - if: ${{ '9' != matrix.java || 'Windows' != runner.os }} - working-directory: pljava - shell: bash - run: | - pgConfig=pg_config # runner-supplied, just get it from the PATH - - packageJar=$(find pljava-packaging -name pljava-pg*.jar -print) - - mavenRepo="$HOME/.m2/repository" - - saxonVer=$( - find "$mavenRepo/net/sf/saxon/Saxon-HE" \ - -name 'Saxon-HE-*.jar' -print | - sort | - tail -n 1 - ) - saxonVer=${saxonVer%/*} - saxonVer=${saxonVer##*/} - - jdbcJar=$( - find "$mavenRepo/com/impossibl/pgjdbc-ng/pgjdbc-ng-all" \ - -name 'pgjdbc-ng-all-*.jar' -print | - sort | - tail -n 1 - ) - - # - # The runner on a Unix-like OS is running as a non-privileged user, but - # has passwordless sudo available (needed to install the PL/Java files - # into the system directories where the supplied PostgreSQL lives). By - # contrast, on Windows the runner has admin privilege, and can install - # the files without any fuss (but later below, pg_ctl will have to be - # used when starting PostgreSQL; pg_ctl has a Windows-specific ability - # to drop admin privs so postgres will not refuse to start). - # - # The Windows runner seems to have an extra pg_config somewhere on the - # path, that reports it was built with MinGW and installed in paths - # containing Strawberry that don't really exist. $PGBIN\pg_config refers - # to a different build made with MSVC, and those directories really - # exist, so specify that one explicitly when running on Windows. - # - # The Git for Windows bash environment includes a find command, and the - # things found have unixy paths returned. Make them Windowsy here, with - # a hardcoded assumption they start with /c which should become c: (as - # appears to be the case in the Windows runner currently). - # - if [[ $RUNNER_OS == Windows ]] - then - pathSep=';' - pgConfig="$PGBIN"'\pg_config' - java -Dpgconfig="$pgConfig" -jar "$packageJar" - function toWindowsPath() { - local p - p="c:${1#/c}" - printf "%s" "${p//\//\\}" - } - jdbcJar="$(toWindowsPath "$jdbcJar")" - mavenRepo="$(toWindowsPath "$mavenRepo")" - else - pathSep=':' - sudo "$JAVA_HOME"/bin/java -Dpgconfig="$pgConfig" -jar "$packageJar" - fi - - jshell \ - -execution local \ - "-J--class-path=$packageJar$pathSep$jdbcJar" \ - "--class-path=$packageJar" \ - "-J--add-modules=java.sql.rowset,jdk.httpserver" \ - "-J-Dpgconfig=$pgConfig" \ - "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ - "-J-DmavenRepo=$mavenRepo" \ - "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL - - boolean succeeding = false; // begin pessimistic - - import static java.nio.file.Files.createTempFile; - import static java.nio.file.Files.write; - import java.nio.file.Path; - import static java.nio.file.Paths.get; - import java.sql.Connection; - import java.sql.PreparedStatement; - import java.sql.ResultSet; - import org.postgresql.pljava.packaging.Node; - import static org.postgresql.pljava.packaging.Node.q; - import static org.postgresql.pljava.packaging.Node.stateMachine; - import static org.postgresql.pljava.packaging.Node.isVoidResultSet; - import static org.postgresql.pljava.packaging.Node.s_isWindows; - import static - org.postgresql.pljava.packaging.Node.NOTHING_OR_PGJDBC_ZERO_COUNT; - /* - * Imports that will be needed to serve a jar file over http - * when the time comes for testing that. - */ - import static java.nio.charset.StandardCharsets.UTF_8; - import java.util.jar.Attributes; - import java.util.jar.Manifest; - import java.util.jar.JarOutputStream; - import java.util.zip.ZipEntry; - import com.sun.net.httpserver.BasicAuthenticator; - import com.sun.net.httpserver.HttpContext; - import com.sun.net.httpserver.HttpExchange; - import com.sun.net.httpserver.HttpHandler; - import com.sun.net.httpserver.HttpServer; - - String javaHome = System.getProperty("java.home"); - - Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); - - Path libjvm = ( - "Mac OS X".equals(System.getProperty("os.name")) - ? Stream.of("libjli.dylib", "jli/libjli.dylib") - .map(s -> javaLibDir.resolve(s)) - .filter(Files::exists).findFirst().get() - : javaLibDir.resolve(s_isWindows ? "jvm.dll" : "server/libjvm.so") - ); - - String vmopts = - "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - - if ( 17 < Runtime.version().feature() ) - vmopts += " -Djava.security.manager=allow"; - - Node n1 = Node.get_new_node("TestNode1"); - - if ( s_isWindows ) - n1.use_pg_ctl(true); - - /* - * Keep a tally of the three types of diagnostic notices that may be - * received, and, independently, how many represent no-good test results - * (error always, but also warning if seen from the tests in the - * examples.jar deployment descriptor). - */ - Map results = - Stream.of("info", "warning", "error", "ng").collect( - LinkedHashMap::new, - (m,k) -> m.put(k, 0), (r,s) -> {}); - - boolean isDiagnostic(Object o, Set whatIsNG) - { - if ( ! ( o instanceof Throwable ) ) - return false; - String[] parts = Node.classify((Throwable)o); - String type = parts[0]; - String message = parts[2]; - results.compute(type, (k,v) -> 1 + v); - if ( whatIsNG.contains(type) ) - if ( ! "warning".equals(type) - || ! message.startsWith("[JEP 411]") ) - results.compute("ng", (k,v) -> 1 + v); - return true; - } - - /* - * Write a trial policy into a temporary file in n's data_dir, - * and set pljava.vmoptions accordingly over connection c. - * Returns the 'succeeding' flag from the state machine looking - * at the command results. - */ - boolean useTrialPolicy(Node n, Connection c, List contents) - throws Exception - { - Path trialPolicy = - createTempFile(n.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, contents); - - PreparedStatement setVmOpts = c.prepareStatement( - "SELECT null::pg_catalog.void" + - " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" - ); - - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); - - return stateMachine( - "change pljava.vmoptions", - null, - - q(setVmOpts, setVmOpts::execute) - .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 - ); - } - - try ( - AutoCloseable t1 = n1.initialized_cluster(); - AutoCloseable t2 = n1.started_server(Map.of( - "client_min_messages", "info", - "pljava.vmoptions", vmopts, - "pljava.libjvm_location", libjvm.toString() - )); - ) - { - try ( Connection c = n1.connect() ) - { - succeeding = true; // become optimistic, will be using &= below - - succeeding &= stateMachine( - "create extension no result", - null, - - q(c, "CREATE EXTENSION pljava") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - // state 1: consume any diagnostics, or to state 2 with same item - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - - NOTHING_OR_PGJDBC_ZERO_COUNT, // state 2 - - // state 3: must be end of input - (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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - 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 <= majorVersion ) - { - 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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - succeeding &= stateMachine( - "package after loading", - null, - - q(c, 13 > majorVersion - ? "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); - ENDJSHELL diff --git a/.travis.yml b/.travis.yml index 3f3d9f36b..9af39d682 100644 --- a/.travis.yml +++ b/.travis.yml @@ -119,7 +119,7 @@ install: | "$mvn" clean install --batch-mode \ -Dpgsql.pgconfig="$pgConfig" \ -Dpljava.libjvmdefault="$libjvm" \ - -Psaxon-examples -Ppgjdbc-ng \ + -Psaxon-examples -Ppgjdbc \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn script: | @@ -136,8 +136,8 @@ script: | saxonVer=${saxonVer##*/} jdbcJar=$( - find "$mavenRepo/com/impossibl/pgjdbc-ng/pgjdbc-ng-all" \ - -name 'pgjdbc-ng-all-*.jar' -print | + find "$mavenRepo/org/postgresql/postgresql" \ + -name 'postgresql-*.jar' -print | sort | tail -n 1 ) @@ -150,349 +150,7 @@ script: | "--class-path=$packageJar" \ "-J--add-modules=java.sql.rowset" \ "-J-Dpgconfig=$pgConfig" \ - "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" \ "-J-DmavenRepo=$mavenRepo" \ - "-J-DsaxonVer=$saxonVer" - <<\ENDJSHELL && # continues after here document - - boolean succeeding = false; // begin pessimistic - - import static java.nio.file.Files.createTempFile - import static java.nio.file.Files.write - import java.nio.file.Path - import static java.nio.file.Paths.get - import java.sql.Connection - import java.sql.PreparedStatement - import java.sql.ResultSet - import org.postgresql.pljava.packaging.Node - import static org.postgresql.pljava.packaging.Node.q - import static org.postgresql.pljava.packaging.Node.stateMachine - import static org.postgresql.pljava.packaging.Node.isVoidResultSet - - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni" - - Node n1 = Node.get_new_node("TestNode1") - - /* - * Keep a tally of the three types of diagnostic notices that may be received, - * and, independently, how many represent no-good test results (error always, - * but also warning if seen from the tests in the examples.jar deployment - * descriptor). - */ - Map results = - Stream.of("info", "warning", "error", "ng").collect( - LinkedHashMap::new, (m,k) -> m.put(k, 0), (r,s) -> {}) - - boolean isDiagnostic(Object o, Set whatIsNG) - { - if ( ! ( o instanceof Throwable ) ) - return false; - String[] parts = Node.classify((Throwable)o); - String type = parts[0]; - String message = parts[2]; - results.compute(type, (k,v) -> 1 + v); - if ( whatIsNG.contains(type) ) - if ( ! "warning".equals(type) || ! message.startsWith("[JEP 411]") ) - results.compute("ng", (k,v) -> 1 + v); - return true; - } - - try ( - AutoCloseable t1 = n1.initialized_cluster(); - AutoCloseable t2 = n1.started_server(Map.of( - "client_min_messages", "info", - "pljava.vmoptions", vmopts - )); - ) - { - try ( Connection c = n1.connect() ) - { - succeeding = true; // become optimistic, will be using &= below - - succeeding &= stateMachine( - "create extension no result", - null, - - q(c, "CREATE EXTENSION pljava") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - // state 1: consume any diagnostics, or go to state 2 without consuming - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - - // state 2: must be end of input - (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: maybe 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: maybe 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: maybe 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: maybe 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() ) - { - Path trialPolicy = - createTempFile(n1.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, List.of( - "grant {", - " permission", - " org.postgresql.pljava.policy.TrialPolicy$Permission;", - "};" - )); - - PreparedStatement setVmOpts = c2.prepareStatement( - "SELECT null::pg_catalog.void" + - " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" - ); - - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); - - succeeding &= stateMachine( - "change pljava.vmoptions", - null, - - q(setVmOpts, setVmOpts::execute) - .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 - ); - - 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, System.getProperty("java.home")); - - 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 - } - - /* - * 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, - (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, - (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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - 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, - (o,p,q) -> null == o - ); - - if ( 13 <= majorVersion ) - { - 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, - (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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - succeeding &= stateMachine( - "package after loading", - null, - - q(c, 13 > majorVersion - ? "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, - (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, - (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) - ENDJSHELL - : travis wants something after the end of the here document + "-J-DsaxonVer=$saxonVer" \ + CI/integration + : travis wanted something here at the end once diff --git a/CI/integration b/CI/integration new file mode 100644 index 000000000..fd8da7676 --- /dev/null +++ b/CI/integration @@ -0,0 +1,614 @@ +/* + * Copyright (c) 2020-2025 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. + * + * 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 + * pgjdbc-ng full jar. The PL/Java packaged jar includes a Node.class + * exporting functions not unlike the Perl module once called PostgresNode + * (and now called PostgreSQL::Test::Cluster) in the PostgreSQL distribution. + * The javadocs for Node.class explain the available functions. + * + * When jshell runs this script with -execution local, it needs both a + * --class-path and a -J--class-path argument. The former need only contain + * the PL/Java jar itself, so the contents are visible to jshell. The -J version + * passed to the underlying JVM needs both that jar and the PGJDBC or pgjdbc-ng + * driver jar. The driver classes need not be visible to jshell, but the JVM + * must be able to find them. + * + * Tests included in this script require + * -J--add-modules=java.sql.rowset,jdk.httpserver + * on the jshell command line. + * + * These Java properties must be set (as with -J-Dpgconfig=...) on the jshell + * command line: + * + * pgconfig + * the path to the pg_config executable that will be used to locate + * the PostgreSQL installation to be used in the tests + * mavenRepo + * the topmost directory of the local Maven repository. The Saxon jar + * downloaded as a dependency (when -Psaxon-examples was used on the mvn + * command line for building) will be found in this repository + * saxonVer + * the version of the Saxon library to use (appears in the library jar + * file name and as the name of its containing directory in the repository) + * + * These properties are optional (their absence is equivalent to a setting + * of false): + * + * redirectError + * if true, the standard error stream from the tests will be merged into + * the standard output stream. This can be desirable if this script is + * invoked from Windows PowerShell, which believes a standard error stream + * should only carry Error Records and makes an awful mess of anything else. + * extractFiles + * if true, begin by extracting and installing the PL/Java files from the jar + * into the proper locations indicated by the pg_config executable. If false, + * extraction will be skipped, assumed to have been done in a separate step + * simply running java -jar on the PL/Java packaged jar. Doing the extraction + * 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 + +boolean redirectError = Boolean.getBoolean("redirectError"); + +if ( redirectError ) + System.setErr(System.out); // PowerShell makes a mess of stderr output + +UnaryOperator tweaks = + redirectError ? p -> p.redirectErrorStream(true) : UnaryOperator.identity(); + +import static java.nio.file.Files.createTempFile; +import static java.nio.file.Files.write; +import java.nio.file.Path; +import static java.nio.file.Paths.get; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import org.postgresql.pljava.packaging.Node; +import static org.postgresql.pljava.packaging.Node.q; +import static org.postgresql.pljava.packaging.Node.stateMachine; +import static org.postgresql.pljava.packaging.Node.isVoidResultSet; +import static org.postgresql.pljava.packaging.Node.s_isWindows; +import static + org.postgresql.pljava.packaging.Node.NOTHING_OR_PGJDBC_ZERO_COUNT; +/* + * Imports that will be needed to serve a jar file over http + * when the time comes for testing that. + */ +import static java.nio.charset.StandardCharsets.UTF_8; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; +import com.sun.net.httpserver.BasicAuthenticator; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +if ( Boolean.getBoolean("extractFiles") ) + Node.main(new String[0]); // extract the files + +String javaHome = System.getProperty("java.home"); + +Path javaLibDir = get(javaHome, s_isWindows ? "bin" : "lib"); + +Path libjvm = ( + "Mac OS X".equals(System.getProperty("os.name")) + ? Stream.of("libjli.dylib", "jli/libjli.dylib") + .map(s -> javaLibDir.resolve(s)) + .filter(Files::exists).findFirst().get() + : javaLibDir.resolve(s_isWindows ? "server\\jvm.dll" : "server/libjvm.so") +); + +// Use deprecated major() here because feature() first appears in Java 10 +int jFeatureVersion = Runtime.version().major(); + +String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; + +vmopts += " --limit-modules=org.postgresql.pljava.internal"; + +if ( 24 <= jFeatureVersion ) { + vmopts += " -Djava.security.manager=disallow"; // JEP 486 +} else if ( 18 <= jFeatureVersion ) + vmopts += " -Djava.security.manager=allow"; // JEP 411 + +if ( 23 <= jFeatureVersion ) + vmopts += " --sun-misc-unsafe-memory-access=deny"; // JEP 471 + +if ( 24 <= jFeatureVersion ) + vmopts += " --illegal-native-access=deny"; // JEP 472 + +Map serverOptions = new HashMap<>(Map.of( + "client_min_messages", "info", + "pljava.vmoptions", vmopts, + "pljava.libjvm_location", libjvm.toString() +)); +if ( 24 <= jFeatureVersion ) { + serverOptions.put("pljava.allow_unenforced", "java,java_tzset"); + serverOptions.put("pljava.allow_unenforced_udt", "on"); +} + +Node n1 = Node.get_new_node("TestNode1"); + +if ( s_isWindows ) + n1.use_pg_ctl(true); + +/* + * Keep a tally of the three types of diagnostic notices that may be + * received, and, independently, how many represent no-good test results + * (error always, but also warning if seen from the tests in the + * examples.jar deployment descriptor). + */ +Map results = + Stream.of("info", "warning", "error", "ng").collect( + LinkedHashMap::new, + (m,k) -> m.put(k, 0), (r,s) -> {}); + +boolean isDiagnostic(Object o, Set whatIsNG) +{ + if ( ! ( o instanceof Throwable ) ) + return false; + String[] parts = Node.classify((Throwable)o); + String type = parts[0]; + String message = parts[2]; + results.compute(type, (k,v) -> 1 + v); + if ( whatIsNG.contains(type) ) + if ( ! "warning".equals(type) || ! message.startsWith("[JEP 411]") ) + results.compute("ng", (k,v) -> 1 + v); + return true; +} + +/* + * Write a trial policy into a temporary file in n's data_dir, + * and set pljava.vmoptions accordingly over connection c. + * Returns the 'succeeding' flag from the state machine looking + * at the command results. + */ +boolean useTrialPolicy(Node n, Connection c, List contents) +throws Exception +{ + Path trialPolicy = + createTempFile(n.data_dir().getParent(), "trial", "policy"); + + write(trialPolicy, contents); + + PreparedStatement setVmOpts = c.prepareStatement( + "SELECT null::pg_catalog.void" + + " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" + ); + + setVmOpts.setString(1, vmopts + + " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); + + return stateMachine( + "change pljava.vmoptions", + null, + + q(setVmOpts, setVmOpts::execute) + .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 + ); +} + +try ( + AutoCloseable t1 = n1.initialized_cluster(tweaks); + AutoCloseable t2 = n1.started_server(serverOptions, tweaks); +) +{ + int pgMajorVersion; + + try ( Connection c = n1.connect() ) + { + pgMajorVersion = c.getMetaData().getDatabaseMajorVersion(); + + succeeding = true; // become optimistic, will be using &= below + + succeeding &= stateMachine( + "create extension no result", + null, + + q(c, "CREATE EXTENSION pljava") + .flatMap(Node::semiFlattenDiagnostics) + .peek(Node::peek), + + // state 1: consume any diagnostics, or to state 2 with same item + (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, + + NOTHING_OR_PGJDBC_ZERO_COUNT, // state 2 + + // state 3: must be end of input + (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 + ); + } + } + + /* + * 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/appveyor.yml b/appveyor.yml index d5edb9c53..acdea6078 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -65,519 +65,19 @@ test_script: Select-Object -Last 1 ).Directory.Name - $jdbcJar = (Join-Path $mavenRepo "com\impossibl\pgjdbc-ng\pgjdbc-ng-all" | - Get-ChildItem -Recurse -Filter pgjdbc-ng-all-*.jar | + $jdbcJar = (Join-Path $mavenRepo "org\postgresql\postgresql" | + Get-ChildItem -Recurse -Filter postgresql-*.jar | Select-Object -Last 1 ).FullName - @' - boolean succeeding = false; // begin pessimistic - - import static java.nio.file.Files.createTempFile; - import static java.nio.file.Files.write; - import java.nio.file.Path; - import static java.nio.file.Paths.get; - import java.sql.Connection; - import java.sql.PreparedStatement; - import java.sql.ResultSet; - import org.postgresql.pljava.packaging.Node; - import static org.postgresql.pljava.packaging.Node.q; - import static org.postgresql.pljava.packaging.Node.stateMachine; - import static org.postgresql.pljava.packaging.Node.isVoidResultSet; - import static - org.postgresql.pljava.packaging.Node.NOTHING_OR_PGJDBC_ZERO_COUNT; - /* - * Imports that will be needed to serve a jar file over http - * when the time comes for testing that. - */ - import static java.nio.charset.StandardCharsets.UTF_8; - import java.util.jar.Attributes; - import java.util.jar.Manifest; - import java.util.jar.JarOutputStream; - import java.util.zip.ZipEntry; - import com.sun.net.httpserver.BasicAuthenticator; - import com.sun.net.httpserver.HttpContext; - import com.sun.net.httpserver.HttpExchange; - import com.sun.net.httpserver.HttpHandler; - import com.sun.net.httpserver.HttpServer; - - System.setErr(System.out); // PowerShell makes a mess of stderr output - - Node.main(new String[0]); // Extract the files (with output to stdout) - - String vmopts = "-enableassertions:org.postgresql.pljava... -Xcheck:jni"; - - if ( 17 < Runtime.version().feature() ) - vmopts += " -Djava.security.manager=allow"; - - Node n1 = Node.get_new_node("TestNode1"); - - n1.use_pg_ctl(true); - - /* - * Keep a tally of the three types of diagnostic notices that may be - * received, and, independently, how many represent no-good test results - * (error always, but also warning if seen from the tests in the - * examples.jar deployment descriptor). - */ - Map results = - Stream.of("info", "warning", "error", "ng").collect( - LinkedHashMap::new, - (m,k) -> m.put(k, 0), (r,s) -> {}); - - boolean isDiagnostic(Object o, Set whatIsNG) - { - if ( ! ( o instanceof Throwable ) ) - return false; - String[] parts = Node.classify((Throwable)o); - String type = parts[0]; - String message = parts[2]; - results.compute(type, (k,v) -> 1 + v); - if ( whatIsNG.contains(type) ) - if ( ! "warning".equals(type) || ! message.startsWith("[JEP 411]") ) - results.compute("ng", (k,v) -> 1 + v); - return true; - } - - /* - * Write a trial policy into a temporary file in n's data_dir, - * and set pljava.vmoptions accordingly over connection c. - * Returns the 'succeeding' flag from the state machine looking - * at the command results. - */ - boolean useTrialPolicy(Node n, Connection c, List contents) - throws Exception - { - Path trialPolicy = - createTempFile(n.data_dir().getParent(), "trial", "policy"); - - write(trialPolicy, contents); - - PreparedStatement setVmOpts = c.prepareStatement( - "SELECT null::pg_catalog.void" + - " FROM pg_catalog.set_config('pljava.vmoptions', ?, false)" - ); - - setVmOpts.setString(1, vmopts + - " -Dorg.postgresql.pljava.policy.trial=" + trialPolicy.toUri()); - - return stateMachine( - "change pljava.vmoptions", - null, - - q(setVmOpts, setVmOpts::execute) - .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 - ); - } - - try ( - AutoCloseable t1 = n1.initialized_cluster( - p->p.redirectErrorStream(true)); - AutoCloseable t2 = n1.started_server(Map.of( - "client_min_messages", "info", - "pljava.vmoptions", vmopts - ), p->p.redirectErrorStream(true)); - ) - { - try ( Connection c = n1.connect() ) - { - succeeding = true; // become optimistic, will be using &= below - - succeeding &= stateMachine( - "create extension no result", - null, - - q(c, "CREATE EXTENSION pljava") - .flatMap(Node::semiFlattenDiagnostics) - .peek(Node::peek), - - // state 1: consume any diagnostics, or show same item to state 2 - (o,p,q) -> isDiagnostic(o, Set.of("error")) ? 1 : -2, - - NOTHING_OR_PGJDBC_ZERO_COUNT, // state 2 - - // state 3: must be end of input - (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, System.getProperty("java.home")); - - 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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - 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 <= majorVersion ) - { - 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() ) - { - int majorVersion = c.getMetaData().getDatabaseMajorVersion(); - - succeeding &= stateMachine( - "package after loading", - null, - - q(c, 13 > majorVersion - ? "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); - '@ | jshell ` -execution local ` "-J--class-path=$packageJar;$jdbcJar" ` "--class-path=$packageJar" ` "-J--add-modules=java.sql.rowset,jdk.httpserver" ` - "-J-Dcom.impossibl.shadow.io.netty.noUnsafe=true" ` "-J-Dpgconfig=$pgConfig" ` "-J-DmavenRepo=$mavenRepo" ` - "-J-DsaxonVer=$saxonVer" - + "-J-DsaxonVer=$saxonVer" ` + "-J-DredirectError=true" ` + "-J-DextractFiles=true" ` + CI\integration diff --git a/pljava-api/src/main/java/org/postgresql/pljava/Session.java b/pljava-api/src/main/java/org/postgresql/pljava/Session.java index 4dbc96947..2169bb644 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/Session.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/Session.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2022 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 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 @@ -17,6 +17,8 @@ import java.sql.Connection; import java.sql.SQLException; +import java.util.Properties; + /** * A Session brings together some useful methods and data for the current * database session. It provides a set of attributes (a @@ -35,6 +37,21 @@ */ public interface Session { + /** + * Returns an unmodifiable defensive copy of the Java + * {@link System#getProperties() system properties} taken early in PL/Java + * startup before user code has an opportunity to write them. + *

+ * When PL/Java is running without security policy enforcement, as on stock + * Java 24 and later, using the frozen properties can simplify defensive + * coding against the possibility of arbitrary property modifications. + * + * @return a {@link Properties} object that departs from the API spec by + * throwing {@link UnsupportedOperationException} from any method if the + * properties would otherwise be modified. + */ + Properties frozenSystemProperties(); + /** * Adds the specified {@code listener} to the list of listeners that will * receive savepoint events. An {@link AccessControlContext} saved by this diff --git a/pljava-api/src/main/java/org/postgresql/pljava/SessionManager.java b/pljava-api/src/main/java/org/postgresql/pljava/SessionManager.java index 5a8c0ed77..3211e4db0 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/SessionManager.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/SessionManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 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 @@ -23,21 +23,41 @@ */ public class SessionManager { - private static Session s_session; - /** * Returns the current session. */ public static Session current() throws SQLException { - if(s_session == null) + try + { + return Holder.s_session; + } + catch ( ExceptionInInitializerError e ) { - s_session = load( - Session.class.getModule().getLayer(), Session.class) - .findFirst().orElseThrow(() -> new SQLException( - "could not obtain PL/Java Session object")); + Throwable c = e.getCause(); + if ( c instanceof SQLException ) + throw (SQLException)c; + throw e; + } + } + + private static class Holder + { + private static final Session s_session; + + static { + try + { + s_session = load( + Session.class.getModule().getLayer(), Session.class) + .findFirst().orElseThrow(() -> new SQLException( + "could not obtain PL/Java Session object")); + } + catch ( SQLException e ) + { + throw new ExceptionInInitializerError(e); + } } - return s_session; } } diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLAction.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLAction.java index face77719..a1ff47377 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLAction.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/SQLAction.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 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,14 +22,33 @@ /** * Annotation that supplies verbatim commands to be copied into the * deployment descriptor. - * - * Strings supplied within a single SQLAction annotation will be copied - * in the order supplied. Strings from different SQLAction annotations, and - * generated code for functions, will be assembled in an order that can be - * influenced by 'provides' and 'requires' labels. No snippet X will be - * emitted ahead of any snippets that provide what X requires. The "remove" - * actions will be assembled in the reverse of that order. - * + *

+ * Strings supplied to {@link #install install} or {@link #remove remove} within + * a single {@code SQLAction} annotation become code snippets emitted into the + * deployment descriptor's {@code INSTALL} or {@code REMOVE} section, + * respectively, in the order supplied. + *

+ * Snippets from different {@code SQLAction} annotations, + * and snippets generated by annotations on functions, types, and such, will be + * assembled in an order that can be influenced by {@link #provides provides} + * and {@link #requires requires} labels. No snippet X will be emitted as an + * {@code INSTALL} action ahead of any snippets that provide what X requires. + * The sense of that dependency is reversed when ordering {@code REMOVE} + * snippets. + *

Conditional execution

+ *

+ * An {@code SQLAction} may supply an {@code install} snippet that tests some + * condition at the time of deployment and adjusts the + * {@code pljava.implementors} setting to include or not include a specific + * {@code }, controlling whether actions later in + * the deployment descriptor that are annotated with that + * {@code } will be executed. The {@code SQLAction} that + * controls whether an {@code } will be recognized should use + * {@link #provides provides} with exactly that name, which is implicitly + * 'required' by statements that use that name as + * {@link #implementor implementor}. For details on this usage, which involves + * a different ordering rule, see "conditional execution" in + * {@link org.postgresql.pljava.annotation the package documentation}. * @author Thomas Hallgren - pre-Java6 version * @author Chapman Flack (Purdue Mathematics) - updated to Java6, * added SQLAction @@ -58,6 +77,10 @@ * generated in such an order that other objects that 'require' labels * 'provided' by this come later in the output for install actions, and * earlier for remove actions. + *

+ * For use of this element on an {@code SQLAction} that tests a condition + * to control conditional execution, see "conditional execution" in + * {@link SQLAction the class description}. */ String[] provides() default {}; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java index ca5af21cf..932113bdd 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2025 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 @@ -13,6 +13,7 @@ /** * Annotations for use in Java code to generate the SQLJ Deployment Descriptor * automatically. + *

Eliminating error-prone hand-maintained SQL scripts

*

* To define functions or types in PL/Java requires more than one step. The * Java code must be written, compiled to a jar, and made available to the @@ -22,14 +23,14 @@ * version that undoes it when uninstalling the jar) can be written in a * prescribed form and stored inside the jar itself as an "SQLJ Deployment * Descriptor", and processed automatically when the jar is installed in or - * removed from the backend. + * removed from the DBMS. *

* To write the deployment descriptor by hand can be tedious and error-prone, * as it must largely duplicate the method and type declarations in the * Java code, but using SQL's syntax and types in place of Java's. Instead, * when the annotations in this package are used in the Java code, the Java - * compiler itself will generate a deployment descriptor file, ready to include - * with the compiled classes to make a complete SQLJ jar. + * compiler itself will generate a deployment descriptor (DDR) file, ready to + * include with the compiled classes to make a complete SQLJ jar. *

* Automatic descriptor generation requires attention to a few things. *

    @@ -37,40 +38,50 @@ * (All but the simplest PL/Java functions probably refer to some class in * PL/Java's API anyway, in which case the jar would already have to be on * the class path.) + *
  • Java compilers older than Java 23 will automatically find and use + * PL/Java's DDR processor as long as the {@code pljava-api} jar is on the class + * path. Starting in Java 23, the compiler will not do so automatically, and a + * {@code -processor org.postgresql.pljava.annotation.processing.DDRProcessor} + * option is also needed on the {@code javac} command line. (Warnings about this + * are issued starting in Java 21, though the processor is still used + * automatically, with the warnings, until Java 23.) *
  • When recompiling after changing only a few sources, it is possible the * Java compiler will only process a subset of the source files containing * annotations. If so, it may generate an incomplete deployment descriptor, * and a clean build may be required to ensure the complete descriptor is * written. - *
  • Additional options are available when invoking the Java compiler, and - * can be specified with -Aoption=value on the command line: + *
+ *

New compiler options when generating the deployment descriptor

+ *

Additional options are available when invoking the Java compiler, and + * can be specified with {@code -Aoption=value} on the command line: *

- *
ddr.output + *
{@code ddr.output} *
The file name to be used for the generated deployment descriptor. * If not specified, the file will be named pljava.ddr and found * in the top directory of the tree where the compiled class files are written. - *
ddr.name.trusted + *
{@code ddr.name.trusted} *
The language name that will be used to declare methods that are * annotated to have {@link org.postgresql.pljava.annotation.Function.Trust#SANDBOXED} behavior. If not - * specified, the name java will be used. It must match the name + * specified, the name {@code java} will be used. It must match the name * used for the "trusted" language declaration when PL/Java was installed. - *
ddr.name.untrusted + *
{@code ddr.name.untrusted} *
The language name that will be used to declare methods that are * annotated to have {@link org.postgresql.pljava.annotation.Function.Trust#UNSANDBOXED} behavior. If not - * specified, the name javaU will be used. It must match the name + * specified, the name {@code javaU} will be used. It must match the name * used for the "untrusted" language declaration when PL/Java was installed. - *
ddr.implementor + *
{@code ddr.implementor} *
The identifier (defaulting to {@code PostgreSQL} if not specified here) * that will be used in the {@code }s wrapping any SQL * generated from elements that do not specify their own. If this is set to a * single hyphen (-), elements that specify no implementor will produce plain * {@code }s not wrapped in {@code }s. - *
ddr.reproducible + *
{@code ddr.reproducible} *
When {@code true} (the default), SQL statements are written to the * deployment descriptor in an order meant to be consistent across successive * compilations of the same sources. This option is further discussed below. *
- *
  • The deployment descriptor may contain statements that cannot succeed if + *

    Controlling order of statements in the deployment descriptor

    + *

    The deployment descriptor may contain statements that cannot succeed if * placed in the wrong order, and to keep a manually-edited script in a workable * order while adding and modifying code can be difficult. Most of the * annotations in this package accept arbitrary {@code requires} and @@ -80,12 +91,13 @@ * compiler, except that it will make sure not to write anything that * {@code requires} some string X into the generated script * before whatever {@code provides} it. - *

  • There can be multiple ways to order the statements in the deployment + *

    Effect of {@code ddr.reproducible}

    + *

    There can be multiple ways to order the statements in the deployment * descriptor to satisfy the given {@code provides} and {@code requires} * relationships. While the compiler will always write the descriptor in an * order that satisfies those relationships, when the {@code ddr.reproducible} * option is {@code false}, the precise order may differ between successive - * compilations of the same sources, which should not affect successful + * compilations of the same sources, which should not affect successful * loading and unloading of the jar with {@code install_jar} and * {@code remove_jar}. In testing, this can help to confirm that all of the * needed {@code provides} and {@code requires} relationships have been @@ -94,6 +106,74 @@ * orders, chosen arbitrarily but consistently between multiple compilations as * long as the sources are unchanged. This can be helpful in software * distribution when reproducible output is wanted. + *

    Conditional execution in the deployment descriptor

    + *

    The deployment-descriptor syntax fixed by the ISO SQL/JRT standard has + * a rudimentary conditional-inclusion feature based on + * {@code }s. + * SQL statements wrapped in {@code BEGIN}/{@code END} with an + * {@code } are executed only if that name is recognized + * by the DBMS when installing or removing the jar. Statements in the deployment + * descriptor that are not wrapped in an {@code } are + * executed unconditionally. + *

    PL/Java's descriptor generator normally emits statements + * as {@code }s, using the name {@code PostgreSQL} + * (or the value of the {@code ddr.implementor} option if present on + * the compiler command line) by default, or a specific name supplied + * with {@code implementor=} to one of the annotations in this package. + *

    When loading or unloading a jar file and processing its deployment + * descriptor, PL/Java 'recognizes' any implementor name listed in the runtime + * setting {@code pljava.implementors}, which contains only {@code PostgreSQL} + * by default. + *

    The {@code pljava.implementors} setting can be changed, even by SQL + * statements within a deployment descriptor, to affect which subsequent + * statements will be executed. An SQL statement may test some condition and + * set {@code pljava.implementors} accordingly. In PL/Java's supplied examples, + * ConditionalDDR illustrates this approach to conditional execution. + *

    Naturally, this scheme requires the SQL generator to emit the statement + * that tests the condition earlier in the deployment descriptor than + * the statements relying on the {@code } being set. + * Building on the existing ability to control the order of statements + * using {@code provides} and {@code requires} elements, an {@code implementor} + * element specified in the annotation for a statement is treated also as + * an implicit {@code requires} for that name, so the programmer only needs + * to place an explicit {@code provides} element on whatever + * {@link SQLAction SQLAction} tests the condition and determines if the name + * will be recognized. + *

    The {@code provides}/{@code requires} relationship so created differs + * in three ways from other {@code provides}/{@code requires} relationships: + *

      + *
    • It does not reverse for generating {@code remove} actions. + * Normal dependencies must be reversed for that case, so dependent objects + * are removed before those they depend on. By contrast, a condition determining + * the setting of an implementor name must be evaluated before the name + * is needed, whether the jar is being installed or removed. + *
    • If it does not have an explicit {@code remove} action (the usual case), + * its {@code install} action (the condition test and setting of the name) + * is used both when installing and removing. + *
    • It is weak. The SQL generator does not flag an error if the implicit + * {@code requires} for an implementor name is not satisfied by any annotation's + * {@code provides} in the visible Java sources. It is possible the name may be + * set some other way in the DBMS environment where the jar is to be deployed. + * Faced with statements that require such 'unprovided' implementor names, + * the SQL generator just falls back to emitting them as late in the deployment + * descriptor as possible, after all other statements that do not depend + * on them. *
    + *

    Matching {@code implementor} and {@code provides}

    + *

    Given the 'weak' nature of the {@code implementor}/{@code provides} + * relationship, an error will not be reported if a spelling or upper/lower case + * difference prevents identifying an {@code } with the + * {@code provides} string of an annotated statement intended to match it. + * The resulting deployment descriptor may have a workable order + * as a result of the fallback ordering rules, or may have a mysteriously + * unworkable order, particularly of the {@code remove} actions. + *

    According to the ISO SQL/JRT standard, an {@code } is + * an SQL identifier, having a case-insensitive matching behavior unless quoted. + * PL/Java, however, treats a {@code provides} value as an arbitrary Java string + * that can only match exactly, and so PL/Java's SQL generator will successfully + * match up {@code implementor} and {@code provides} strings only when + * they are identical in spelling and case. */ package org.postgresql.pljava.annotation; diff --git a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java index 262ec227d..2da7b0bc5 100644 --- a/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java +++ b/pljava-api/src/main/java/org/postgresql/pljava/annotation/processing/DDRProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2024 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 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 @@ -6074,7 +6074,7 @@ interface Snippet */ default DependTag implementorTag() { - return new DependTag.Explicit(implementorName().pgFolded()); + return new DependTag.Explicit(implementorName().nonFolded()); } /** * Return an array of SQL commands (one complete command to a string) to diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java index 21375c909..26fabe830 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/ConditionalDDR.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2015-2025 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 @@ -24,38 +24,51 @@ * that are not tagged with an implementor name). The default setting of * {@code pljava.implementors} is simply {@code postgresql}. *

    - * In this example, an SQLAction (with the default implementor name PostgreSQL - * so it should always execute) tests some condition and, based on the result, - * adds {@code LifeIsGood} to the list of recognized implementor names. + * In this example, an {@code SQLAction} (with the default implementor name + * {@code PostgreSQL} so it should always execute) tests some condition and, + * based on the result, adds {@code LifeIsGood} to the list of recognized + * implementor names. *

    - * Later SQLActions with that implementor name should also be executed, while - * those with a different, unrecognized implementor should not. + * Later {@code SQLAction}s with that implementor name should also be executed, + * while those with a different, unrecognized implementor should not. *

    * That is what happens at deployment (or undeployment) time, when the * jar has been loaded into the target database and the deployment descriptor is * being processed. *

    - * The {@code provides} and {@code requires} attributes matter at + * The {@code provides} attributes matter at * compile time: they are hints to the DDR generator so it will be sure - * to write the SQLAction that tests the condition ahead of the ones that - * depend on the condition having been tested. The example illustrates that an - * SQLAction's {@code implementor} is treated as an implicit {@code requires}. - * Unlike an explicit one, it is weak: if there is nothing declared that - * {@code provides} it, that's not an error; affected SQLActions will just be - * placed as late in the generated DDR as other dependencies allow, in case - * something in the preceding actions will be setting those implementor tags. + * to write the {@code SQLAction} that tests the condition ahead of whatever + * depends on the condition having been tested. The example illustrates that + * {@code implementor} is treated also as an implicit {@code requires}. *

    - * The implicit {@code requires} derived from an {@code implementor} is also - * special in another way: it does not have its sense reversed when generating - * the "undeploy" actions of the deployment descriptor. Ordinary requirements - * do, so the dependent objects get dropped before the things they depend on. - * But the code for setting a conditional implementor tag has to be placed - * ahead of the uses of the tag, whether deploying or undeploying. + * Note: while ISO SQL/JRT specifies that an {@code } is an + * SQL identifier, which would match case-insensitively unless quoted, PL/Java + * treats {@code provides} elements as arbitrary strings that can only be + * matched with identical spelling and case. Therefore, the matching of the + * implicit {@code requires} of an {@code } and the explicit + * {@code provides} on an {@code SQLAction} depends on the {@code implementor} + * and {@code provides} values being supplied with identical spelling and case, *

    - * An {@code SQLAction} setting an implementor tag does not need to have any - * {@code remove=} actions. If it does not (the usual case), its + * The dependency created when matching {@code implementor} to {@code provides} + * differs in three ways from an explicit dependency between {@code requires} + * and {@code provides}: + *

      + *
    • It is weak: if there is nothing declared that {@code provides} it, + * that's not an error; affected {@code }s will just be + * placed as late in the generated DDR as other dependencies allow, in case + * something in the preceding actions will be setting those implementor names. + *
    • It does not have its sense reversed when generating + * the {@code REMOVE} actions of the deployment descriptor. Ordinary + * requirements do, so the dependent objects get dropped before the things they + * depend on. + * But the code for setting a conditional implementor name has to be placed + * ahead of the uses of the name, whether deploying or undeploying. + *
    • An {@code SQLAction} setting an implementor name does not need to have + * any {@code remove=} actions. If it does not (the usual case), its * {@code install=} actions will be used in both sections of the deployment * descriptor. + *
    *

    * This example adds {@code LifeIsGood} ahead of the prior content of * {@code pljava.implementors}. Simply replacing the value would stop the @@ -64,8 +77,8 @@ * local, so it is reverted when the transaction completes. *

    * In addition to the goodness-of-life examples, this file also generates - * one or more statements setting PostgreSQL-version-based implementor tags that - * are relied on by various other examples in this directory. + * one or more statements setting PostgreSQL-version-based implementor names + * that are relied on by various other examples in this directory. */ @SQLAction(provides={"LifeIsGood","LifeIsNotGood"}, install= "SELECT CASE 42 WHEN 42 THEN " + diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java index ff2c23116..dde2eaabf 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/JDBC42_21.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2025 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 @@ -11,6 +11,10 @@ */ package org.postgresql.pljava.example.annotation; +import java.sql.SQLException; + +import org.postgresql.pljava.SessionManager; + import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.SQLAction; @@ -133,9 +137,10 @@ public class JDBC42_21 * recent as the argument ('1.6', '1.7', '1.8', '9', '10', '11', ...). */ @Function(schema="javatest", provides="javaSpecificationGE") - public static boolean javaSpecificationGE(String want) + public static boolean javaSpecificationGE(String want) throws SQLException { - String got = System.getProperty("java.specification.version"); + String got = SessionManager.current().frozenSystemProperties() + .getProperty("java.specification.version"); if ( want.startsWith("1.") ) want = want.substring(2); if ( got.startsWith("1.") ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Modules.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Modules.java new file mode 100644 index 000000000..b2e9826a1 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/Modules.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 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 + */ +package org.postgresql.pljava.example.annotation; + +import java.lang.module.ModuleDescriptor; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import java.util.Iterator; +import java.util.Objects; + +import java.util.stream.Stream; + +import org.postgresql.pljava.ResultSetProvider; +import org.postgresql.pljava.annotation.Function; +import static org.postgresql.pljava.annotation.Function.Effects.STABLE; + +/** + * Example code to support querying for the modules in Java's boot layer. + */ +public class Modules implements ResultSetProvider.Large { + /** + * Returns information on the named modules in Java's boot module layer. + */ + @Function( + effects = STABLE, + out = { + "name pg_catalog.text", + "any_unqualified_exports boolean", + "any_unqualified_opens boolean" + } + ) + public static ResultSetProvider java_modules() + { + return new Modules( + ModuleLayer.boot().modules().stream().map(Module::getDescriptor) + .filter(Objects::nonNull)); + } + + private final Iterator iterator; + private final Runnable closer; + + private Modules(Stream s) + { + iterator = s.iterator(); + closer = s::close; + } + + @Override + public boolean assignRowValues(ResultSet receiver, long currentRow) + throws SQLException + { + if ( ! iterator.hasNext() ) + return false; + + ModuleDescriptor md = iterator.next(); + + receiver.updateString(1, md.name()); + + receiver.updateBoolean(2, + md.exports().stream().anyMatch(e -> ! e.isQualified())); + + receiver.updateBoolean(3, + md.isOpen() || + md.opens().stream().anyMatch(o -> ! o.isQualified())); + + return true; + } + + @Override + public void close() + { + closer.run(); + } +} diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java index 5f3886886..d6dd14bfc 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/PassXML.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024 Tada AB and other contributors, as listed below. + * Copyright (c) 2018-2025 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 @@ -67,6 +67,7 @@ import org.postgresql.pljava.Adjusting; import static org.postgresql.pljava.Adjusting.XML.setFirstSupported; +import org.postgresql.pljava.SessionManager; import org.postgresql.pljava.annotation.Function; import org.postgresql.pljava.annotation.MappedUDT; import org.postgresql.pljava.annotation.SQLAction; @@ -643,7 +644,8 @@ public static SQLXML transformXML( */ if ( rlt instanceof StreamResult ) t.setOutputProperty(ENCODING, - System.getProperty("org.postgresql.server.encoding")); + SessionManager.current().frozenSystemProperties() + .getProperty("org.postgresql.server.encoding")); else if ( Boolean.TRUE.equals(indent) ) logMessage("WARNING", "indent requested, but howout specifies a non-stream " + @@ -712,7 +714,8 @@ private static SQLXML echoSQLXML(SQLXML sx, int howin, int howout) */ if ( howout < 5 ) t.setOutputProperty(ENCODING, - System.getProperty("org.postgresql.server.encoding")); + SessionManager.current().frozenSystemProperties() + .getProperty("org.postgresql.server.encoding")); t.transform(src, rlt); } catch ( TransformerException te ) diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SPIActions.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SPIActions.java index dca34e5c7..baf3861e6 100644 --- a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SPIActions.java +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/SPIActions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 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 @@ -127,20 +127,26 @@ public static String getTimeAsString() throws SQLException { } } - static void log(String msg) { + static void log(String msg) throws SQLException { // GCJ has a somewhat serious bug (reported) // - if ("GNU libgcj".equals(System.getProperty("java.vm.name"))) { + if ("GNU libgcj" + .equals( + SessionManager.current().frozenSystemProperties() + .getProperty("java.vm.name"))) { System.out.print("INFO: "); System.out.println(msg); } else Logger.getAnonymousLogger().info(msg); } - static void warn(String msg) { + static void warn(String msg) throws SQLException { // GCJ has a somewhat serious bug (reported) // - if ("GNU libgcj".equals(System.getProperty("java.vm.name"))) { + if ("GNU libgcj" + .equals( + SessionManager.current().frozenSystemProperties() + .getProperty("java.vm.name"))) { System.out.print("WARNING: "); System.out.println(msg); } else diff --git a/pljava-so/pom.xml b/pljava-so/pom.xml index ab7c30bf6..172feb52c 100644 --- a/pljava-so/pom.xml +++ b/pljava-so/pom.xml @@ -1,5 +1,9 @@ - + 4.0.0 org.postgresql @@ -9,7 +13,8 @@ pljava-so PL/Java backend native code - Generates the pljava (.so, .dll, etc.) library which gets loaded by the PostgreSQL backend + Generates the pljava (.so, .dll, etc.) library which gets loaded + by the PostgreSQL backend pom @@ -28,325 +33,337 @@